Index: /trunk/CONTRIBUTION
===================================================================
--- /trunk/CONTRIBUTION	(revision 8131)
+++ /trunk/CONTRIBUTION	(revision 8132)
@@ -35,5 +35,5 @@
 
 The jpeg metadata extraction code is from Drew Noakes
-(http://code.google.com/p/metadata-extractor/) and licensed
+(https://github.com/drewnoakes/metadata-extractor) and licensed
 with Apache license version 2.0.
 
Index: /trunk/src/com/drew/imaging/ImageProcessingException.java
===================================================================
--- /trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 8131)
+++ /trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.imaging;
@@ -26,6 +26,6 @@
 /**
  * An exception class thrown upon an unexpected condition that was fatal for the processing of an image.
- * 
- * @author Drew Noakes http://drewnoakes.com
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ImageProcessingException extends CompoundException
Index: /trunk/src/com/drew/imaging/PhotographicConversions.java
===================================================================
--- /trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 8131)
+++ /trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.imaging;
@@ -24,5 +24,5 @@
  * Contains helper methods that perform photographic conversions.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public final class PhotographicConversions
Index: /trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 8131)
+++ /trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,46 +16,76 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.imaging.jpeg;
 
-import com.drew.lang.ByteArrayReader;
-import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Metadata;
-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;
-
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.drew.lang.StreamReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Metadata;
+//import com.drew.metadata.adobe.AdobeJpegReader;
+import com.drew.metadata.exif.ExifReader;
+//import com.drew.metadata.icc.IccReader;
+import com.drew.metadata.iptc.IptcReader;
+//import com.drew.metadata.jfif.JfifReader;
+import com.drew.metadata.jpeg.JpegCommentReader;
+import com.drew.metadata.jpeg.JpegReader;
+//import com.drew.metadata.photoshop.PhotoshopReader;
+//import com.drew.metadata.xmp.XmpReader;
 
 /**
- * Obtains all available metadata from Jpeg formatted files.
+ * Obtains all available metadata from JPEG formatted files.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://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{}
-//    public static Metadata readMetadata(IIOImage image) throws JpegProcessingException{}
-//    public static Metadata readMetadata(ImageReader reader) throws JpegProcessingException{}
+    public static final Iterable<JpegSegmentMetadataReader> ALL_READERS = Arrays.asList(
+            new JpegReader(),
+            new JpegCommentReader(),
+            //new JfifReader(),
+            new ExifReader(),
+            //new XmpReader(),
+            //new IccReader(),
+            //new PhotoshopReader(),
+            new IptcReader()//,
+            //new AdobeJpegReader()
+    );
 
     @NotNull
-    public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException
+    public static Metadata readMetadata(@NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
     {
-        return readMetadata(inputStream, true);
+        Metadata metadata = new Metadata();
+        process(metadata, inputStream, readers);
+        return metadata;
     }
 
     @NotNull
-    public static Metadata readMetadata(@NotNull InputStream inputStream, final boolean waitForBytes) throws JpegProcessingException
+    public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException, IOException
     {
-        JpegSegmentReader segmentReader = new JpegSegmentReader(inputStream, waitForBytes);
-        return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData());
+        return readMetadata(inputStream, null);
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
+    {
+        InputStream inputStream = null;
+        try
+        {
+            inputStream = new FileInputStream(file);
+            return readMetadata(inputStream, readers);
+        } finally {
+            if (inputStream != null)
+                inputStream.close();
+        }
     }
 
@@ -63,54 +93,41 @@
     public static Metadata readMetadata(@NotNull File file) throws JpegProcessingException, IOException
     {
-        JpegSegmentReader segmentReader = new JpegSegmentReader(file);
-        return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData());
+        return readMetadata(file, null);
     }
 
-    @NotNull
-    public static Metadata extractMetadataFromJpegSegmentReader(@NotNull JpegSegmentData segmentReader)
+    public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream) throws JpegProcessingException, IOException
     {
-        final Metadata metadata = new Metadata();
+        process(metadata, inputStream, null);
+    }
 
-        // 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;
-        }
+    public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
+    {
+        if (readers == null)
+            readers = ALL_READERS;
 
-        // 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);
-        }
-
-        // 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);
+        Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>();
+        for (JpegSegmentMetadataReader reader : readers) {
+            for (JpegSegmentType type : reader.getSegmentTypes()) {
+                segmentTypes.add(type);
             }
         }
 
-        return metadata;
+        JpegSegmentData segmentData = JpegSegmentReader.readSegments(new StreamReader(inputStream), segmentTypes);
+
+        processJpegSegmentData(metadata, readers, segmentData);
+    }
+
+    public static void processJpegSegmentData(Metadata metadata, Iterable<JpegSegmentMetadataReader> readers, JpegSegmentData segmentData)
+    {
+        // Pass the appropriate byte arrays to each reader.
+        for (JpegSegmentMetadataReader reader : readers) {
+            for (JpegSegmentType segmentType : reader.getSegmentTypes()) {
+                for (byte[] segmentBytes : segmentData.getSegments(segmentType)) {
+                    if (reader.canProcess(segmentBytes, segmentType)) {
+                        reader.extract(segmentBytes, metadata, segmentType);
+                    }
+                }
+            }
+        }
     }
 
@@ -120,3 +137,2 @@
     }
 }
-
Index: /trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 8131)
+++ /trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.imaging.jpeg;
@@ -25,7 +25,7 @@
 
 /**
- * An exception class thrown upon unexpected and fatal conditions while processing a Jpeg file.
+ * An exception class thrown upon unexpected and fatal conditions while processing a JPEG file.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class JpegProcessingException extends ImageProcessingException
Index: /trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 8131)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.imaging.jpeg;
@@ -24,24 +24,23 @@
 import com.drew.lang.annotations.Nullable;
 
-import java.io.*;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
+import java.util.*;
 
 /**
- * Holds a collection of Jpeg data segments.  This need not necessarily be all segments
- * 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
+ * Holds a collection of JPEG data segments.  This need not necessarily be all segments
+ * within the JPEG. For example, it may be convenient to store only the non-image
+ * segments when analysing metadata.
+ * <p>
+ * Segments are keyed via their {@link JpegSegmentType}. Where multiple segments use the
+ * same segment type, they will all be stored and available.
+ * <p>
+ * Each segment type may contain multiple entries. Conceptually the model is:
+ * <code>Map&lt;JpegSegmentType, Collection&lt;byte[]&gt;&gt;</code>. This class provides
+ * convenience methods around that structure.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
-public class JpegSegmentData implements Serializable
+public class JpegSegmentData
 {
-    private static final long serialVersionUID = 7110175216435025451L;
-    
-    /** A map of byte[], keyed by the segment marker */
+    // TODO key this on JpegSegmentType rather than Byte, and hopefully lose much of the use of 'byte' with this class
     @NotNull
     private final HashMap<Byte, List<byte[]>> _segmentDataMap = new HashMap<Byte, List<byte[]>>(10);
@@ -49,71 +48,129 @@
     /**
      * 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
+     *
+     * @param segmentType  the type of the segment being added
+     * @param segmentBytes the byte array holding data for the segment being added
+     */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
+    public void addSegment(byte segmentType, @NotNull byte[] segmentBytes)
+    {
+        getOrCreateSegmentList(segmentType).add(segmentBytes);
+    }
+
+    /**
+     * Gets the set of JPEG segment type identifiers.
+     */
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>();
+
+        for (Byte segmentTypeByte : _segmentDataMap.keySet())
+        {
+            JpegSegmentType segmentType = JpegSegmentType.fromByte(segmentTypeByte);
+            if (segmentType == null) {
+                throw new IllegalStateException("Should not have a segmentTypeByte that is not in the enum: " + Integer.toHexString(segmentTypeByte));
+            }
+            segmentTypes.add(segmentType);
+        }
+
+        return segmentTypes;
+    }
+
+    /**
+     * Gets the first JPEG segment data for the specified type.
+     *
+     * @param segmentType the JpegSegmentType 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)
-    {
-        return getSegment(segmentMarker, 0);
-    }
-
-    /**
-     * 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<byte[]> segmentList = getSegmentList(segmentMarker);
-
-        if (segmentList==null || segmentList.size()<=occurrence)
-            return null;
-        else
-            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)
+    public byte[] getSegment(byte segmentType)
+    {
+        return getSegment(segmentType, 0);
+    }
+
+    /**
+     * Gets the first JPEG segment data for the specified type.
+     *
+     * @param segmentType the JpegSegmentType for the desired segment
+     * @return a byte[] containing segment data or null if no data exists for that segment
+     */
+    @Nullable
+    public byte[] getSegment(@NotNull JpegSegmentType segmentType)
+    {
+        return getSegment(segmentType.byteValue, 0);
+    }
+
+    /**
+     * Gets segment data for a specific occurrence and type.  Use this method when more than one occurrence
+     * of segment data for a given type exists.
+     *
+     * @param segmentType 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 type &amp; occurrence
+     */
+    @Nullable
+    public byte[] getSegment(@NotNull JpegSegmentType segmentType, int occurrence)
+    {
+        return getSegment(segmentType.byteValue, occurrence);
+    }
+
+    /**
+     * Gets segment data for a specific occurrence and type.  Use this method when more than one occurrence
+     * of segment data for a given type exists.
+     *
+     * @param segmentType 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 type &amp; occurrence
+     */
+    @Nullable
+    public byte[] getSegment(byte segmentType, int occurrence)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentType);
+
+        return segmentList != null && segmentList.size() > occurrence
+                ? segmentList.get(occurrence)
+                : null;
+    }
+
+    /**
+     * Returns all instances of a given JPEG segment.  If no instances exist, an empty sequence is returned.
+     *
+     * @param segmentType 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(@NotNull JpegSegmentType segmentType)
+    {
+        return getSegments(segmentType.byteValue);
+    }
+
+    /**
+     * Returns all instances of a given JPEG segment.  If no instances exist, an empty sequence is returned.
+     *
+     * @param segmentType 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 segmentType)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentType);
+        return segmentList == null ? new ArrayList<byte[]>() : segmentList;
+    }
+
+    @Nullable
+    private List<byte[]> getSegmentList(byte segmentType)
+    {
+        return _segmentDataMap.get(segmentType);
+    }
+
+    @NotNull
+    private List<byte[]> getOrCreateSegmentList(byte segmentType)
     {
         List<byte[]> segmentList;
-        if (_segmentDataMap.containsKey(segmentMarker)) {
-            segmentList = _segmentDataMap.get(segmentMarker);
+        if (_segmentDataMap.containsKey(segmentType)) {
+            segmentList = _segmentDataMap.get(segmentType);
         } else {
             segmentList = new ArrayList<byte[]>();
-            _segmentDataMap.put(segmentMarker, segmentList);
+            _segmentDataMap.put(segmentType, segmentList);
         }
         return segmentList;
@@ -121,11 +178,23 @@
 
     /**
-     * Returns the count of segment data byte arrays stored for a given segment marker.
-     * @param segmentMarker identifies the required segment
+     * Returns the count of segment data byte arrays stored for a given segment type.
+     *
+     * @param segmentType identifies the required segment
      * @return the segment count (zero if no segments exist).
      */
-    public int getSegmentCount(byte segmentMarker)
-    {
-        final List<byte[]> segmentList = getSegmentList(segmentMarker);
+    public int getSegmentCount(@NotNull JpegSegmentType segmentType)
+    {
+        return getSegmentCount(segmentType.byteValue);
+    }
+
+    /**
+     * Returns the count of segment data byte arrays stored for a given segment type.
+     *
+     * @param segmentType identifies the required segment
+     * @return the segment count (zero if no segments exist).
+     */
+    public int getSegmentCount(byte segmentType)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentType);
         return segmentList == null ? 0 : segmentList.size();
     }
@@ -133,76 +202,69 @@
     /**
      * 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<byte[]> segmentList = _segmentDataMap.get(Byte.valueOf(segmentMarker));
+     * occurrence of segment data exists for a given type exists.
+     *
+     * @param segmentType identifies the required segment
+     * @param occurrence  the zero-based index of the segment occurrence to remove.
+     */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
+    public void removeSegmentOccurrence(@NotNull JpegSegmentType segmentType, int occurrence)
+    {
+        removeSegmentOccurrence(segmentType.byteValue, occurrence);
+    }
+
+    /**
+     * Removes a specified instance of a segment's data from the collection.  Use this method when more than one
+     * occurrence of segment data exists for a given type exists.
+     *
+     * @param segmentType identifies the required segment
+     * @param occurrence  the zero-based index of the segment occurrence to remove.
+     */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
+    public void removeSegmentOccurrence(byte segmentType, int occurrence)
+    {
+        final List<byte[]> segmentList = _segmentDataMap.get(segmentType);
         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(Byte.valueOf(segmentMarker));
-    }
-
-    /**
-     * Determines whether data is present for a given segment marker.
-     * @param segmentMarker identifies the required segment
+     * Removes all segments from the collection having the specified type.
+     *
+     * @param segmentType identifies the required segment
+     */
+    public void removeSegment(@NotNull JpegSegmentType segmentType)
+    {
+        removeSegment(segmentType.byteValue);
+    }
+
+    /**
+     * Removes all segments from the collection having the specified type.
+     *
+     * @param segmentType identifies the required segment
+     */
+    public void removeSegment(byte segmentType)
+    {
+        _segmentDataMap.remove(segmentType);
+    }
+
+    /**
+     * Determines whether data is present for a given segment type.
+     *
+     * @param segmentType identifies the required segment
      * @return true if data exists, otherwise false
      */
-    public boolean containsSegment(byte segmentMarker)
-    {
-        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
-        {
-            fileOutputStream = new FileOutputStream(file);
-            new ObjectOutputStream(fileOutputStream).writeObject(segmentData);
-        }
-        finally
-        {
-            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;
-        try
-        {
-            inputStream = new ObjectInputStream(new FileInputStream(file));
-            return (JpegSegmentData)inputStream.readObject();
-        }
-        finally
-        {
-            if (inputStream!=null)
-                inputStream.close();
-        }
+    public boolean containsSegment(@NotNull JpegSegmentType segmentType)
+    {
+        return containsSegment(segmentType.byteValue);
+    }
+
+    /**
+     * Determines whether data is present for a given segment type.
+     *
+     * @param segmentType identifies the required segment
+     * @return true if data exists, otherwise false
+     */
+    public boolean containsSegment(byte segmentType)
+    {
+        return _segmentDataMap.containsKey(segmentType);
     }
 }
Index: /trunk/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java	(revision 8132)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java	(revision 8132)
@@ -0,0 +1,32 @@
+package com.drew.imaging.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+/**
+ * Defines an object that extracts metadata from in JPEG segments.
+ */
+public interface JpegSegmentMetadataReader
+{
+    /**
+     * Gets the set of JPEG segment types that this reader is interested in.
+     */
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes();
+
+    /**
+     * Gets a value indicating whether the supplied byte data can be processed by this reader. This is not a guarantee
+     * that no errors will occur, but rather a best-effort indication of whether the parse is likely to succeed.
+     * Implementations are expected to check things such as the opening bytes, data length, etc.
+     */
+    public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType);
+
+    /**
+     * Extracts metadata from a JPEG segment's byte array and merges it into the specified {@link Metadata} object.
+     *
+     * @param segmentBytes The byte array from which the metadata should be extracted.
+     * @param metadata The {@link Metadata} object into which extracted values should be merged.
+     * @param segmentType The {@link JpegSegmentType} being read.
+     */
+    public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType);
+}
Index: /trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 8131)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,278 +16,153 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.imaging.jpeg;
 
+import com.drew.lang.SequentialReader;
+import com.drew.lang.StreamReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 
-import java.io.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
- * Performs read functions of Jpeg files, returning specific file segments.
- * @author  Drew Noakes http://drewnoakes.com
+ * Performs read functions of JPEG files, returning specific file segments.
+ * <p>
+ * JPEG files are composed of a sequence of consecutive JPEG 'segments'. Each is identified by one of a set of byte
+ * values, modelled in the {@link JpegSegmentType} enumeration. Use <code>readSegments</code> to read out the some
+ * or all segments into a {@link JpegSegmentData} object, from which the raw JPEG segment byte arrays may be accessed.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class JpegSegmentReader
 {
-    // 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;
+    private static final byte SEGMENT_SOS = (byte) 0xDA;
 
     /**
      * Private, because one wouldn't search for it.
      */
-    private static final byte MARKER_EOI = (byte)0xD9;
-
-    /** 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.  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. */
-    public static final byte SEGMENT_APP2 = (byte)0xE2;
-    /** APP3 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP3 = (byte)0xE3;
-    /** APP4 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP4 = (byte)0xE4;
-    /** APP5 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP5 = (byte)0xE5;
-    /** APP6 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP6 = (byte)0xE6;
-    /** APP7 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP7 = (byte)0xE7;
-    /** APP8 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP8 = (byte)0xE8;
-    /** APP9 Jpeg segment identifier. */
-    public static final byte SEGMENT_APP9 = (byte)0xE9;
-    /** APPA (App10) Jpeg segment identifier -- can hold Unicode comments. */
-    public static final byte SEGMENT_APPA = (byte)0xEA;
-    /** APPB (App11) Jpeg segment identifier. */
-    public static final byte SEGMENT_APPB = (byte)0xEB;
-    /** APPC (App12) Jpeg segment identifier. */
-    public static final byte SEGMENT_APPC = (byte)0xEC;
-    /** APPD (App13) Jpeg segment identifier -- IPTC data in here. */
-    public static final byte SEGMENT_APPD = (byte)0xED;
-    /** APPE (App14) Jpeg segment identifier. */
-    public static final byte SEGMENT_APPE = (byte)0xEE;
-    /** APPF (App15) Jpeg segment identifier. */
-    public static final byte SEGMENT_APPF = (byte)0xEF;
-    /** Start Of Image segment identifier. */
-    public static final byte SEGMENT_SOI = (byte)0xD8;
-    /** Define Quantization Table segment identifier. */
-    public static final byte SEGMENT_DQT = (byte)0xDB;
-    /** Define Huffman Table segment identifier. */
-    public static final byte SEGMENT_DHT = (byte)0xC4;
-    /** Start-of-Frame Zero segment identifier. */
-    public static final byte SEGMENT_SOF0 = (byte)0xC0;
-    /** Jpeg comment segment identifier. */
-    public static final byte SEGMENT_COM = (byte)0xFE;
+    private static final byte MARKER_EOI = (byte) 0xD9;
 
     /**
-     * Creates a JpegSegmentReader for a specific file.
-     * @param file the Jpeg file to read segments from
+     * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object.
+     * <p>
+     * Will not return SOS (start of scan) or EOI (end of image) segments.
+     *
+     * @param file a {@link File} from which the JPEG data will be read.
+     * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code>
+     *                     then all found segment types are returned.
      */
-    @SuppressWarnings({ "ConstantConditions" })
-    public JpegSegmentReader(@NotNull File file) throws JpegProcessingException, IOException
+    @NotNull
+    public static JpegSegmentData readSegments(@NotNull File file, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException
     {
-        if (file==null)
-            throw new NullPointerException();
-
-        InputStream inputStream = null;
+        FileInputStream stream = null;
         try {
-            inputStream = new FileInputStream(file);
-            _segmentData = readSegments(new BufferedInputStream(inputStream), false);
+            stream = new FileInputStream(file);
+            return readSegments(new StreamReader(stream), segmentTypes);
         } finally {
-            if (inputStream != null)
-                inputStream.close();
+            if (stream != null) {
+                stream.close();
+            }
         }
     }
 
     /**
-     * Creates a JpegSegmentReader for a byte array.
-     * @param fileContents the byte array containing Jpeg data
+     * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object.
+     * <p>
+     * Will not return SOS (start of scan) or EOI (end of image) segments.
+     *
+     * @param reader a {@link SequentialReader} from which the JPEG data will be read. It must be positioned at the
+     *               beginning of the JPEG data stream.
+     * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code>
+     *                     then all found segment types are returned.
      */
-    @SuppressWarnings({ "ConstantConditions" })
-    public JpegSegmentReader(@NotNull byte[] fileContents) throws JpegProcessingException
+    @NotNull
+    public static JpegSegmentData readSegments(@NotNull final SequentialReader reader, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException
     {
-        if (fileContents==null)
-            throw new NullPointerException();
+        // Must be big-endian
+        assert (reader.isMotorolaByteOrder());
 
-        BufferedInputStream stream = new BufferedInputStream(new ByteArrayInputStream(fileContents));
-        _segmentData = readSegments(stream, false);
+        // first two bytes should be JPEG magic number
+        final int magicNumber = reader.getUInt16();
+        if (magicNumber != 0xFFD8) {
+            throw new JpegProcessingException("JPEG data is expected to begin with 0xFFD8 (ÿØ) not 0x" + Integer.toHexString(magicNumber));
+        }
+
+        Set<Byte> segmentTypeBytes = null;
+        if (segmentTypes != null) {
+            segmentTypeBytes = new HashSet<Byte>();
+            for (JpegSegmentType segmentType : segmentTypes) {
+                segmentTypeBytes.add(segmentType.byteValue);
+            }
+        }
+
+        JpegSegmentData segmentData = new JpegSegmentData();
+
+        do {
+            // Find the segment marker. Markers are zero or more 0xFF bytes, followed
+            // by a 0xFF and then a byte not equal to 0x00 or 0xFF.
+
+            final short segmentIdentifier = reader.getUInt8();
+
+            // We must have at least one 0xFF byte
+            if (segmentIdentifier != 0xFF)
+                throw new JpegProcessingException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase());
+
+            // Read until we have a non-0xFF byte. This identifies the segment type.
+            byte segmentType = reader.getInt8();
+            while (segmentType == (byte)0xFF)
+                segmentType = reader.getInt8();
+
+            if (segmentType == 0)
+                throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier");
+
+            if (segmentType == SEGMENT_SOS) {
+                // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
+                // have to search for the two bytes: 0xFF 0xD9 (EOI).
+                // It comes last so simply return at this point
+                return segmentData;
+            }
+
+            if (segmentType == MARKER_EOI) {
+                // the 'End-Of-Image' segment -- this should never be found in this fashion
+                return segmentData;
+            }
+
+            // next 2-bytes are <segment-size>: [high-byte] [low-byte]
+            int segmentLength = reader.getUInt16();
+
+            // segment length includes size bytes, so subtract two
+            segmentLength -= 2;
+
+            if (segmentLength < 0)
+                throw new JpegProcessingException("JPEG segment size would be less than zero");
+
+            // Check whether we are interested in this segment
+            if (segmentTypeBytes == null || segmentTypeBytes.contains(segmentType)) {
+                byte[] segmentBytes = reader.getBytes(segmentLength);
+                assert (segmentLength == segmentBytes.length);
+                segmentData.addSegment(segmentType, segmentBytes);
+            } else {
+                // Some if the JPEG is truncated, just return what data we've already gathered
+                if (!reader.trySkip(segmentLength)) {
+                    return segmentData;
+                }
+            }
+
+        } while (true);
     }
 
-    /**
-     * Creates a JpegSegmentReader for an InputStream.
-     * @param inputStream the InputStream containing Jpeg data
-     */
-    @SuppressWarnings({ "ConstantConditions" })
-    public JpegSegmentReader(@NotNull InputStream inputStream, boolean waitForBytes) throws JpegProcessingException
+    private JpegSegmentReader() throws Exception
     {
-        if (inputStream==null)
-            throw new NullPointerException();
-
-        BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream
-                ? (BufferedInputStream)inputStream
-                : new BufferedInputStream(inputStream);
-
-        _segmentData = readSegments(bufferedInputStream, waitForBytes);
-    }
-
-    /**
-     * Reads the first instance of a given Jpeg segment, returning the contents as
-     * a byte array.
-     * @param segmentMarker the byte identifier for the desired segment
-     * @return the byte array if found, else null
-     */
-    @Nullable
-    public byte[] readSegment(byte segmentMarker)
-    {
-        return readSegment(segmentMarker, 0);
-    }
-
-    /**
-     * 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)
-    {
-        return _segmentData.getSegment(segmentMarker, 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[]> 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)
-    {
-        return _segmentData.getSegmentCount(segmentMarker);
-    }
-
-    /**
-     * Returns the JpegSegmentData object used by this reader.
-     * @return the JpegSegmentData object.
-     */
-    @NotNull
-    public final JpegSegmentData getSegmentData()
-    {
-        return _segmentData;
-    }
-
-    @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
-            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)(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));
-                }
-                offset++;
-                // next byte is <segment-marker>
-                byte thisSegmentMarker = (byte)(jpegInputStream.read() & 0xFF);
-                offset++;
-                // next 2-bytes are <segment-size>: [high-byte] [low-byte]
-                byte[] segmentLengthBytes = new byte[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 (!checkForBytesOnStream(jpegInputStream, segmentLength, waitForBytes))
-                    throw new JpegProcessingException("segment size would extend beyond file stream length");
-                if (segmentLength < 0)
-                    throw new JpegProcessingException("segment size would be less than zero");
-                byte[] segmentBytes = new byte[segmentLength];
-                if (jpegInputStream.read(segmentBytes, 0, segmentLength) != segmentLength)
-                    throw new JpegProcessingException("Jpeg data ended unexpectedly.");
-                offset += segmentLength;
-                if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) {
-                    // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
-                    // have to search for the two bytes: 0xFF 0xD9 (EOI).
-                    // It comes last so simply return at this point
-                    return segmentData;
-                } else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) {
-                    // the 'End-Of-Image' segment -- this should never be found in this fashion
-                    return segmentData;
-                } else {
-                    segmentData.addSegment(thisSegmentMarker, segmentBytes);
-                }
-            } while (true);
-        } catch (IOException ioe) {
-            throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
-        } finally {
-            try {
-                if (jpegInputStream != null) {
-                    jpegInputStream.close();
-                }
-            } catch (IOException ioe) {
-                throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
-            }
-        }
-    }
-
-    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--;
-        }
-        return false;
+        throw new Exception("Not intended for instantiation.");
     }
 }
Index: /trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 8132)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 8132)
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.jpeg;
+
+import com.drew.lang.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An enumeration of the known segment types found in JPEG files.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public enum JpegSegmentType
+{
+    /** APP0 JPEG segment identifier -- JFIF data (also JFXX apparently). */
+    APP0((byte)0xE0, true),
+
+    /** APP1 JPEG segment identifier -- where Exif data is kept.  XMP data is also kept in here, though usually in a second instance. */
+    APP1((byte)0xE1, true),
+
+    /** APP2 JPEG segment identifier. */
+    APP2((byte)0xE2, true),
+
+    /** APP3 JPEG segment identifier. */
+    APP3((byte)0xE3, true),
+
+    /** APP4 JPEG segment identifier. */
+    APP4((byte)0xE4, true),
+
+    /** APP5 JPEG segment identifier. */
+    APP5((byte)0xE5, true),
+
+    /** APP6 JPEG segment identifier. */
+    APP6((byte)0xE6, true),
+
+    /** APP7 JPEG segment identifier. */
+    APP7((byte)0xE7, true),
+
+    /** APP8 JPEG segment identifier. */
+    APP8((byte)0xE8, true),
+
+    /** APP9 JPEG segment identifier. */
+    APP9((byte)0xE9, true),
+
+    /** APPA (App10) JPEG segment identifier -- can hold Unicode comments. */
+    APPA((byte)0xEA, true),
+
+    /** APPB (App11) JPEG segment identifier. */
+    APPB((byte)0xEB, true),
+
+    /** APPC (App12) JPEG segment identifier. */
+    APPC((byte)0xEC, true),
+
+    /** APPD (App13) JPEG segment identifier -- IPTC data in here. */
+    APPD((byte)0xED, true),
+
+    /** APPE (App14) JPEG segment identifier. */
+    APPE((byte)0xEE, true),
+
+    /** APPF (App15) JPEG segment identifier. */
+    APPF((byte)0xEF, true),
+
+    /** Start Of Image segment identifier. */
+    SOI((byte)0xD8, false),
+
+    /** Define Quantization Table segment identifier. */
+    DQT((byte)0xDB, false),
+
+    /** Define Huffman Table segment identifier. */
+    DHT((byte)0xC4, false),
+
+    /** Start-of-Frame (0) segment identifier. */
+    SOF0((byte)0xC0, true),
+
+    /** Start-of-Frame (1) segment identifier. */
+    SOF1((byte)0xC1, true),
+
+    /** Start-of-Frame (2) segment identifier. */
+    SOF2((byte)0xC2, true),
+
+    /** Start-of-Frame (3) segment identifier. */
+    SOF3((byte)0xC3, true),
+
+//    /** Start-of-Frame (4) segment identifier. */
+//    SOF4((byte)0xC4, true),
+
+    /** Start-of-Frame (5) segment identifier. */
+    SOF5((byte)0xC5, true),
+
+    /** Start-of-Frame (6) segment identifier. */
+    SOF6((byte)0xC6, true),
+
+    /** Start-of-Frame (7) segment identifier. */
+    SOF7((byte)0xC7, true),
+
+    /** Start-of-Frame (8) segment identifier. */
+    SOF8((byte)0xC8, true),
+
+    /** Start-of-Frame (9) segment identifier. */
+    SOF9((byte)0xC9, true),
+
+    /** Start-of-Frame (10) segment identifier. */
+    SOF10((byte)0xCA, true),
+
+    /** Start-of-Frame (11) segment identifier. */
+    SOF11((byte)0xCB, true),
+
+//    /** Start-of-Frame (12) segment identifier. */
+//    SOF12((byte)0xCC, true),
+
+    /** Start-of-Frame (13) segment identifier. */
+    SOF13((byte)0xCD, true),
+
+    /** Start-of-Frame (14) segment identifier. */
+    SOF14((byte)0xCE, true),
+
+    /** Start-of-Frame (15) segment identifier. */
+    SOF15((byte)0xCF, true),
+
+    /** JPEG comment segment identifier. */
+    COM((byte)0xFE, true);
+
+    public static final Collection<JpegSegmentType> canContainMetadataTypes;
+
+    static {
+        List<JpegSegmentType> segmentTypes = new ArrayList<JpegSegmentType>();
+        for (JpegSegmentType segmentType : JpegSegmentType.class.getEnumConstants()) {
+            if (segmentType.canContainMetadata) {
+                segmentTypes.add(segmentType);
+            }
+        }
+        canContainMetadataTypes = segmentTypes;
+    }
+
+    public final byte byteValue;
+    public final boolean canContainMetadata;
+
+    JpegSegmentType(byte byteValue, boolean canContainMetadata)
+    {
+        this.byteValue = byteValue;
+        this.canContainMetadata = canContainMetadata;
+    }
+
+    @Nullable
+    public static JpegSegmentType fromByte(byte segmentTypeByte)
+    {
+        for (JpegSegmentType segmentType : JpegSegmentType.class.getEnumConstants()) {
+            if (segmentType.byteValue == segmentTypeByte)
+                return segmentType;
+        }
+        return null;
+    }
+}
Index: /trunk/src/com/drew/imaging/jpeg/package.html
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/package.html	(revision 8132)
+++ /trunk/src/com/drew/imaging/jpeg/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for working with JPEG files.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/imaging/package.html
===================================================================
--- /trunk/src/com/drew/imaging/package.html	(revision 8132)
+++ /trunk/src/com/drew/imaging/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for working with image file formats and photographic conversions.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/imaging/tiff/TiffDataFormat.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 8132)
+++ /trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 8132)
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.tiff;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An enumeration of data formats used by the TIFF specification.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class TiffDataFormat
+{
+    public static final int CODE_INT8_U = 1;
+    public static final int CODE_STRING = 2;
+    public static final int CODE_INT16_U = 3;
+    public static final int CODE_INT32_U = 4;
+    public static final int CODE_RATIONAL_U = 5;
+    public static final int CODE_INT8_S = 6;
+    public static final int CODE_UNDEFINED = 7;
+    public static final int CODE_INT16_S = 8;
+    public static final int CODE_INT32_S = 9;
+    public static final int CODE_RATIONAL_S = 10;
+    public static final int CODE_SINGLE = 11;
+    public static final int CODE_DOUBLE = 12;
+
+    @NotNull public static final TiffDataFormat INT8_U = new TiffDataFormat("BYTE", CODE_INT8_U, 1);
+    @NotNull public static final TiffDataFormat STRING = new TiffDataFormat("STRING", CODE_STRING, 1);
+    @NotNull public static final TiffDataFormat INT16_U = new TiffDataFormat("USHORT", CODE_INT16_U, 2);
+    @NotNull public static final TiffDataFormat INT32_U = new TiffDataFormat("ULONG", CODE_INT32_U, 4);
+    @NotNull public static final TiffDataFormat RATIONAL_U = new TiffDataFormat("URATIONAL", CODE_RATIONAL_U, 8);
+    @NotNull public static final TiffDataFormat INT8_S = new TiffDataFormat("SBYTE", CODE_INT8_S, 1);
+    @NotNull public static final TiffDataFormat UNDEFINED = new TiffDataFormat("UNDEFINED", CODE_UNDEFINED, 1);
+    @NotNull public static final TiffDataFormat INT16_S = new TiffDataFormat("SSHORT", CODE_INT16_S, 2);
+    @NotNull public static final TiffDataFormat INT32_S = new TiffDataFormat("SLONG", CODE_INT32_S, 4);
+    @NotNull public static final TiffDataFormat RATIONAL_S = new TiffDataFormat("SRATIONAL", CODE_RATIONAL_S, 8);
+    @NotNull public static final TiffDataFormat SINGLE = new TiffDataFormat("SINGLE", CODE_SINGLE, 4);
+    @NotNull public static final TiffDataFormat DOUBLE = new TiffDataFormat("DOUBLE", CODE_DOUBLE, 8);
+
+    @NotNull
+    private final String _name;
+    private final int _tiffFormatCode;
+    private final int _componentSizeBytes;
+
+    @Nullable
+    public static TiffDataFormat fromTiffFormatCode(int tiffFormatCode)
+    {
+        switch (tiffFormatCode) {
+            case 1: return INT8_U;
+            case 2: return STRING;
+            case 3: return INT16_U;
+            case 4: return INT32_U;
+            case 5: return RATIONAL_U;
+            case 6: return INT8_S;
+            case 7: return UNDEFINED;
+            case 8: return INT16_S;
+            case 9: return INT32_S;
+            case 10: return RATIONAL_S;
+            case 11: return SINGLE;
+            case 12: return DOUBLE;
+        }
+        return null;
+    }
+
+    private TiffDataFormat(@NotNull String name, int tiffFormatCode, int componentSizeBytes)
+    {
+        _name = name;
+        _tiffFormatCode = tiffFormatCode;
+        _componentSizeBytes = componentSizeBytes;
+    }
+
+    public int getComponentSizeBytes()
+    {
+        return _componentSizeBytes;
+    }
+
+    public int getTiffFormatCode()
+    {
+        return _tiffFormatCode;
+    }
+
+    @Override
+    @NotNull
+    public String toString()
+    {
+        return _name;
+    }
+}
Index: /trunk/src/com/drew/imaging/tiff/TiffHandler.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/TiffHandler.java	(revision 8132)
+++ /trunk/src/com/drew/imaging/tiff/TiffHandler.java	(revision 8132)
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.tiff;
+
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public interface TiffHandler
+{
+    /**
+     * Receives the 2-byte marker found in the TIFF header.
+     * <p>
+     * Implementations are not obligated to use this information for any purpose, though it may be useful for
+     * validation or perhaps differentiating the type of mapping to use for observed tags and IFDs.
+     *
+     * @param marker the 2-byte value found at position 2 of the TIFF header
+     */
+    void setTiffMarker(int marker) throws TiffProcessingException;
+
+    boolean isTagIfdPointer(int tagType);
+    boolean hasFollowerIfd();
+
+    void endingIFD();
+
+    void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset);
+
+    boolean customProcessTag(int tagOffset,
+                             @NotNull Set<Integer> processedIfdOffsets,
+                             int tiffHeaderOffset,
+                             @NotNull RandomAccessReader reader,
+                             int tagId,
+                             int byteCount) throws IOException;
+
+    void warn(@NotNull String message);
+    void error(@NotNull String message);
+
+    void setByteArray(int tagId, @NotNull byte[] bytes);
+    void setString(int tagId, @NotNull String string);
+    void setRational(int tagId, @NotNull Rational rational);
+    void setRationalArray(int tagId, @NotNull Rational[] array);
+    void setFloat(int tagId, float float32);
+    void setFloatArray(int tagId, @NotNull float[] array);
+    void setDouble(int tagId, double double64);
+    void setDoubleArray(int tagId, @NotNull double[] array);
+    void setInt8s(int tagId, byte int8s);
+    void setInt8sArray(int tagId, @NotNull byte[] array);
+    void setInt8u(int tagId, short int8u);
+    void setInt8uArray(int tagId, @NotNull short[] array);
+    void setInt16s(int tagId, int int16s);
+    void setInt16sArray(int tagId, @NotNull short[] array);
+    void setInt16u(int tagId, int int16u);
+    void setInt16uArray(int tagId, @NotNull int[] array);
+    void setInt32s(int tagId, int int32s);
+    void setInt32sArray(int tagId, @NotNull int[] array);
+    void setInt32u(int tagId, long int32u);
+    void setInt32uArray(int tagId, @NotNull long[] array);
+}
Index: /trunk/src/com/drew/imaging/tiff/TiffProcessingException.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 8132)
+++ /trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 8132)
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.imaging.tiff;
+
+import com.drew.imaging.ImageProcessingException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An exception class thrown upon unexpected and fatal conditions while processing a TIFF file.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Darren Salomons
+ */
+public class TiffProcessingException extends ImageProcessingException
+{
+    private static final long serialVersionUID = -1658134119488001891L;
+
+    public TiffProcessingException(@Nullable String message)
+    {
+        super(message);
+    }
+
+    public TiffProcessingException(@Nullable String message, @Nullable Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public TiffProcessingException(@Nullable Throwable cause)
+    {
+        super(cause);
+    }
+}
Index: /trunk/src/com/drew/imaging/tiff/TiffReader.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/TiffReader.java	(revision 8132)
+++ /trunk/src/com/drew/imaging/tiff/TiffReader.java	(revision 8132)
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.tiff;
+
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Processes TIFF-formatted data, calling into client code via that {@link TiffHandler} interface.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class TiffReader
+{
+    /**
+     * Processes a TIFF data sequence.
+     *
+     * @param reader the {@link RandomAccessReader} from which the data should be read
+     * @param handler the {@link TiffHandler} that will coordinate processing and accept read values
+     * @param tiffHeaderOffset the offset within <code>reader</code> at which the TIFF header starts
+     * @throws TiffProcessingException if an error occurred during the processing of TIFF data that could not be
+     *                                 ignored or recovered from
+     * @throws IOException an error occurred while accessing the required data
+     */
+    public void processTiff(@NotNull final RandomAccessReader reader,
+                            @NotNull final TiffHandler handler,
+                            final int tiffHeaderOffset) throws TiffProcessingException, IOException
+    {
+        // This must be either "MM" or "II".
+        short byteOrderIdentifier = reader.getInt16(tiffHeaderOffset);
+
+        if (byteOrderIdentifier == 0x4d4d) { // "MM"
+            reader.setMotorolaByteOrder(true);
+        } else if (byteOrderIdentifier == 0x4949) { // "II"
+            reader.setMotorolaByteOrder(false);
+        } else {
+            throw new TiffProcessingException("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);
+        }
+
+        // Check the next two values for correctness.
+        final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset);
+        handler.setTiffMarker(tiffMarker);
+
+        int firstIfdOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;
+
+        // David Ekholm sent a digital camera image that has this problem
+        // TODO getLength should be avoided as it causes RandomAccessStreamReader to read to the end of the stream
+        if (firstIfdOffset >= reader.getLength() - 1) {
+            handler.warn("First IFD offset is beyond the end of the TIFF data segment -- trying default offset");
+            // First directory normally starts immediately after the offset bytes, so try that
+            firstIfdOffset = tiffHeaderOffset + 2 + 2 + 4;
+        }
+
+        Set<Integer> processedIfdOffsets = new HashSet<Integer>();
+        processIfd(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset);
+
+        handler.completed(reader, tiffHeaderOffset);
+    }
+
+    /**
+     * Processes a TIFF IFD.
+     *
+     * IFD Header:
+     * <ul>
+     *     <li><b>2 bytes</b> number of tags</li>
+     * </ul>
+     * Tag structure:
+     * <ul>
+     *     <li><b>2 bytes</b> tag type</li>
+     *     <li><b>2 bytes</b> format code (values 1 to 12, inclusive)</li>
+     *     <li><b>4 bytes</b> component count</li>
+     *     <li><b>4 bytes</b> inline value, or offset pointer if too large to fit in four bytes</li>
+     * </ul>
+     *
+     *
+     * @param handler the {@link com.drew.imaging.tiff.TiffHandler} that will coordinate processing and accept read values
+     * @param reader the {@link com.drew.lang.RandomAccessReader} from which the data should be read
+     * @param processedIfdOffsets the set of visited IFD offsets, to avoid revisiting the same IFD in an endless loop
+     * @param ifdOffset the offset within <code>reader</code> at which the IFD data starts
+     * @param tiffHeaderOffset the offset within <code>reader</code> at which the TIFF header starts
+     * @throws IOException an error occurred while accessing the required data
+     */
+    public static void processIfd(@NotNull final TiffHandler handler,
+                                  @NotNull final RandomAccessReader reader,
+                                  @NotNull final Set<Integer> processedIfdOffsets,
+                                  final int ifdOffset,
+                                  final int tiffHeaderOffset) throws IOException
+    {
+        try {
+            // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
+            if (processedIfdOffsets.contains(Integer.valueOf(ifdOffset))) {
+                return;
+            }
+
+            // remember that we've visited this directory so that we don't visit it again later
+            processedIfdOffsets.add(ifdOffset);
+
+            if (ifdOffset >= reader.getLength() || ifdOffset < 0) {
+                handler.error("Ignored IFD 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(ifdOffset);
+
+            int dirLength = (2 + (12 * dirTagCount) + 4);
+            if (dirLength + ifdOffset > reader.getLength()) {
+                handler.error("Illegally sized IFD");
+                return;
+            }
+
+            //
+            // Handle each tag in this directory
+            //
+            int invalidTiffFormatCodeCount = 0;
+            for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {
+                final int tagOffset = calculateTagOffset(ifdOffset, tagNumber);
+
+                // 2 bytes for the tag id
+                final int tagId = reader.getUInt16(tagOffset);
+
+                // 2 bytes for the format code
+                final int formatCode = reader.getUInt16(tagOffset + 2);
+                final TiffDataFormat format = TiffDataFormat.fromTiffFormatCode(formatCode);
+
+                if (format == null) {
+                    // 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.
+                    handler.error("Invalid TIFF tag format code: " + formatCode);
+                    // TODO specify threshold as a parameter, or provide some other external control over this behaviour
+                    if (++invalidTiffFormatCodeCount > 5) {
+                        handler.error("Stopping processing as too many errors seen in TIFF IFD");
+                        return;
+                    }
+                    continue;
+                }
+
+                // 4 bytes dictate the number of components in this tag's data
+                final int componentCount = reader.getInt32(tagOffset + 4);
+                if (componentCount < 0) {
+                    handler.error("Negative TIFF tag component count");
+                    continue;
+                }
+
+                final int byteCount = componentCount * format.getComponentSizeBytes();
+
+                final int tagValueOffset;
+                if (byteCount > 4) {
+                    // If it's bigger than 4 bytes, the dir entry contains an offset.
+                    final int offsetVal = reader.getInt32(tagOffset + 8);
+                    if (offsetVal + byteCount > reader.getLength()) {
+                        // Bogus pointer offset and / or byteCount value
+                        handler.error("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()) {
+                    handler.error("Illegal TIFF tag pointer offset");
+                    continue;
+                }
+
+                // 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 > reader.getLength()) {
+                    handler.error("Illegal number of bytes for TIFF tag data: " + byteCount);
+                    continue;
+                }
+
+                //
+                // Special handling for tags that point to other IFDs
+                //
+                if (byteCount == 4 && handler.isTagIfdPointer(tagId)) {
+                    final int subDirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
+                    processIfd(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset);
+                } else {
+                    if (!handler.customProcessTag(tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, byteCount)) {
+                        processTag(handler, tagId, tagValueOffset, componentCount, formatCode, reader);
+                    }
+                }
+            }
+
+            // at the end of each IFD is an optional link to the next IFD
+            final int finalTagOffset = calculateTagOffset(ifdOffset, dirTagCount);
+            int nextIfdOffset = reader.getInt32(finalTagOffset);
+            if (nextIfdOffset != 0) {
+                nextIfdOffset += tiffHeaderOffset;
+                if (nextIfdOffset >= 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
+                    return;
+                } else if (nextIfdOffset < ifdOffset) {
+                    // TODO is this a valid restriction?
+                    // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory
+                    return;
+                }
+
+                if (handler.hasFollowerIfd()) {
+                    processIfd(handler, reader, processedIfdOffsets, nextIfdOffset, tiffHeaderOffset);
+                }
+            }
+        } finally {
+            handler.endingIFD();
+        }
+    }
+
+    private static void processTag(@NotNull final TiffHandler handler,
+                                   final int tagId,
+                                   final int tagValueOffset,
+                                   final int componentCount,
+                                   final int formatCode,
+                                   @NotNull final RandomAccessReader reader) throws IOException
+    {
+        switch (formatCode) {
+            case TiffDataFormat.CODE_UNDEFINED:
+                // this includes exif user comments
+                handler.setByteArray(tagId, reader.getBytes(tagValueOffset, componentCount));
+                break;
+            case TiffDataFormat.CODE_STRING:
+                handler.setString(tagId, reader.getNullTerminatedString(tagValueOffset, componentCount));
+                break;
+            case TiffDataFormat.CODE_RATIONAL_S:
+                if (componentCount == 1) {
+                    handler.setRational(tagId, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));
+                } else if (componentCount > 1) {
+                    Rational[] array = new Rational[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));
+                    handler.setRationalArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_RATIONAL_U:
+                if (componentCount == 1) {
+                    handler.setRational(tagId, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));
+                } else if (componentCount > 1) {
+                    Rational[] array = new Rational[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));
+                    handler.setRationalArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_SINGLE:
+                if (componentCount == 1) {
+                    handler.setFloat(tagId, reader.getFloat32(tagValueOffset));
+                } else {
+                    float[] array = new float[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getFloat32(tagValueOffset + (i * 4));
+                    handler.setFloatArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_DOUBLE:
+                if (componentCount == 1) {
+                    handler.setDouble(tagId, reader.getDouble64(tagValueOffset));
+                } else {
+                    double[] array = new double[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getDouble64(tagValueOffset + (i * 4));
+                    handler.setDoubleArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT8_S:
+                if (componentCount == 1) {
+                    handler.setInt8s(tagId, reader.getInt8(tagValueOffset));
+                } else {
+                    byte[] array = new byte[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getInt8(tagValueOffset + i);
+                    handler.setInt8sArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT8_U:
+                if (componentCount == 1) {
+                    handler.setInt8u(tagId, reader.getUInt8(tagValueOffset));
+                } else {
+                    short[] array = new short[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getUInt8(tagValueOffset + i);
+                    handler.setInt8uArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT16_S:
+                if (componentCount == 1) {
+                    handler.setInt16s(tagId, (int)reader.getInt16(tagValueOffset));
+                } else {
+                    short[] array = new short[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getInt16(tagValueOffset + (i * 2));
+                    handler.setInt16sArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT16_U:
+                if (componentCount == 1) {
+                    handler.setInt16u(tagId, reader.getUInt16(tagValueOffset));
+                } else {
+                    int[] array = new int[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getUInt16(tagValueOffset + (i * 2));
+                    handler.setInt16uArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT32_S:
+                // NOTE 'long' in this case means 32 bit, not 64
+                if (componentCount == 1) {
+                    handler.setInt32s(tagId, reader.getInt32(tagValueOffset));
+                } else {
+                    int[] array = new int[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getInt32(tagValueOffset + (i * 4));
+                    handler.setInt32sArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT32_U:
+                // NOTE 'long' in this case means 32 bit, not 64
+                if (componentCount == 1) {
+                    handler.setInt32u(tagId, reader.getUInt32(tagValueOffset));
+                } else {
+                    long[] array = new long[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getUInt32(tagValueOffset + (i * 4));
+                    handler.setInt32uArray(tagId, array);
+                }
+                break;
+            default:
+                handler.error(String.format("Unknown format code %d for tag %d", formatCode, tagId));
+        }
+    }
+
+    /**
+     * Determine the offset of a given tag within the specified IFD.
+     *
+     * @param ifdStartOffset the offset at which the IFD starts
+     * @param entryNumber    the zero-based entry number
+     */
+    private static int calculateTagOffset(int ifdStartOffset, int entryNumber)
+    {
+        // Add 2 bytes for the tag count.
+        // Each entry is 12 bytes.
+        return ifdStartOffset + 2 + (12 * entryNumber);
+    }
+}
Index: /trunk/src/com/drew/imaging/tiff/package.html
===================================================================
--- /trunk/src/com/drew/imaging/tiff/package.html	(revision 8132)
+++ /trunk/src/com/drew/imaging/tiff/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for working with TIFF format files.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- /trunk/src/com/drew/lang/BufferBoundsException.java	(revision 8131)
+++ /trunk/src/com/drew/lang/BufferBoundsException.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,26 +16,24 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/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
+ * A checked replacement for {@link IndexOutOfBoundsException}.  Used by {@link RandomAccessReader}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
-public final class BufferBoundsException extends Exception
+public final class BufferBoundsException extends IOException
 {
     private static final long serialVersionUID = 2911102837808946396L;
 
-    public BufferBoundsException(@NotNull byte[] buffer, int index, int bytesRequested)
+    public BufferBoundsException(int index, int bytesRequested, long bufferLength)
     {
-        super(getMessage(buffer, index, bytesRequested));
+        super(getMessage(index, bytesRequested, bufferLength));
     }
 
@@ -45,16 +43,17 @@
     }
 
-    public BufferBoundsException(final String message, final IOException innerException)
-    {
-        super(message, innerException);
-    }
-
-    private static String getMessage(@NotNull byte[] buffer, int index, int bytesRequested)
+    private static String getMessage(int index, int bytesRequested, long bufferLength)
     {
         if (index < 0)
-            return String.format("Attempt to read from buffer using a negative index (%s)", index);
+            return String.format("Attempt to read from buffer using a negative index (%d)", 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);
+        if (bytesRequested < 0)
+            return String.format("Number of requested bytes cannot be negative (%d)", bytesRequested);
+
+        if ((long)index + (long)bytesRequested - 1L > (long)Integer.MAX_VALUE)
+            return String.format("Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: %d, requested count: %d)", index, bytesRequested);
+
+        return String.format("Attempt to read from beyond end of underlying data source (requested index: %d, requested count: %d, max index: %d)",
+                index, bytesRequested, bufferLength - 1);
     }
 }
Index: unk/src/com/drew/lang/BufferReader.java
===================================================================
--- /trunk/src/com/drew/lang/BufferReader.java	(revision 8131)
+++ 	(revision )
@@ -1,145 +1,0 @@
-/*
- * 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 8131)
+++ /trunk/src/com/drew/lang/ByteArrayReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -24,20 +24,19 @@
 import com.drew.lang.annotations.NotNull;
 
-import java.io.UnsupportedEncodingException;
+import java.io.IOException;
 
 /**
  * Provides methods to read specific values from a byte array, with a consistent, checked exception structure for
  * issues.
- * <p/>
+ * <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
+ * <code>setMotorolaByteOrder(boolean)</code>.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  * */
-public class ByteArrayReader implements BufferReader
+public class ByteArrayReader extends RandomAccessReader
 {
     @NotNull
     private final byte[] _buffer;
-    private boolean _isMotorolaByteOrder = true;
 
     @SuppressWarnings({ "ConstantConditions" })
@@ -47,5 +46,5 @@
         if (buffer == null)
             throw new NullPointerException();
-        
+
         _buffer = buffer;
     }
@@ -57,171 +56,30 @@
     }
 
-
     @Override
-    public void setMotorolaByteOrder(boolean motorolaByteOrder)
+    protected byte getByte(int index) throws IOException
     {
-        _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
+    protected void validateIndex(int index, int bytesRequested) throws IOException
     {
-        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);
-        }
+        if (!isValidIndex(index, bytesRequested))
+            throw new BufferBoundsException(index, bytesRequested, _buffer.length);
     }
 
     @Override
-    public short getInt16(int index) throws BufferBoundsException
+    protected boolean isValidIndex(int index, int bytesRequested) throws IOException
     {
-        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));
-        }
+        return bytesRequested >= 0
+            && index >= 0
+            && (long)index + (long)bytesRequested - 1L < (long)_buffer.length;
     }
 
     @Override
-    public long getUInt32(int index) throws BufferBoundsException
+    @NotNull
+    public byte[] getBytes(int index, int count) throws IOException
     {
-        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);
+        validateIndex(index, count);
 
         byte[] bytes = new byte[count];
@@ -229,45 +87,3 @@
         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 8131)
+++ /trunk/src/com/drew/lang/CompoundException.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.lang;
@@ -32,5 +32,5 @@
  * of these previous JDK versions.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class CompoundException extends Exception
@@ -63,4 +63,5 @@
     }
 
+    @Override
     @NotNull
     public String toString()
@@ -77,4 +78,5 @@
     }
 
+    @Override
     public void printStackTrace(@NotNull PrintStream s)
     {
@@ -86,4 +88,5 @@
     }
 
+    @Override
     public void printStackTrace(@NotNull PrintWriter s)
     {
@@ -95,4 +98,5 @@
     }
 
+    @Override
     public void printStackTrace()
     {
Index: /trunk/src/com/drew/lang/GeoLocation.java
===================================================================
--- /trunk/src/com/drew/lang/GeoLocation.java	(revision 8131)
+++ /trunk/src/com/drew/lang/GeoLocation.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -25,7 +25,11 @@
 import com.drew.lang.annotations.Nullable;
 
+import java.text.DecimalFormat;
+
 /**
  * Represents a latitude and longitude pair, giving a position on earth in spherical coordinates.
+ * <p>
  * Values of latitude and longitude are given in degrees.
+ * <p>
  * This type is immutable.
  */
@@ -79,5 +83,6 @@
     {
         double[] dms = decimalToDegreesMinutesSeconds(decimal);
-        return dms[0] + "° " + dms[1] + "' " + dms[2] + '"';
+        DecimalFormat format = new DecimalFormat("0.##");
+        return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
     }
 
Index: /trunk/src/com/drew/lang/NullOutputStream.java
===================================================================
--- /trunk/src/com/drew/lang/NullOutputStream.java	(revision 8131)
+++ /trunk/src/com/drew/lang/NullOutputStream.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.lang;
@@ -27,5 +27,5 @@
  * An implementation of OutputSteam that ignores write requests by doing nothing.  This class may be useful in tests.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class NullOutputStream extends OutputStream
@@ -36,4 +36,5 @@
     }
 
+    @Override
     public void write(int b) throws IOException
     {
Index: /trunk/src/com/drew/lang/RandomAccessReader.java
===================================================================
--- /trunk/src/com/drew/lang/RandomAccessReader.java	(revision 8132)
+++ /trunk/src/com/drew/lang/RandomAccessReader.java	(revision 8132)
@@ -0,0 +1,365 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Base class for random access data reading operations of common data types.
+ * <p>
+ * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
+ * {@link com.drew.lang.RandomAccessReader#setMotorolaByteOrder(boolean)}.
+ * <p>
+ * Concrete implementations include:
+ * <ul>
+ *     <li>{@link ByteArrayReader}</li>
+ *     <li>{@link RandomAccessStreamReader}</li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class RandomAccessReader
+{
+    private boolean _isMotorolaByteOrder = true;
+
+    /**
+     * Gets the byte value at the specified byte <code>index</code>.
+     * <p>
+     * Implementations should not perform any bounds checking in this method. That should be performed
+     * in <code>validateIndex</code> and <code>isValidIndex</code>.
+     *
+     * @param index The index from which to read the byte
+     * @return The read byte value
+     * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative
+     * @throws BufferBoundsException if the requested byte is beyond the end of the underlying data source
+     * @throws IOException if the byte is unable to be read
+     */
+    protected abstract byte getByte(int index) throws IOException;
+
+    /**
+     * Returns the required number of bytes from the specified index from the underlying source.
+     *
+     * @param index The index from which the bytes begins in the underlying source
+     * @param count The number of bytes to be returned
+     * @return The requested bytes
+     * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative
+     * @throws BufferBoundsException if the requested bytes extend beyond the end of the underlying data source
+     * @throws IOException if the byte is unable to be read
+     */
+    @NotNull
+    public abstract byte[] getBytes(int index, int count) throws IOException;
+
+    /**
+     * Ensures that the buffered bytes extend to cover the specified index. If not, an attempt is made
+     * to read to that point.
+     * <p>
+     * If the stream ends before the point is reached, a {@link BufferBoundsException} is raised.
+     *
+     * @param index the index from which the required bytes start
+     * @param bytesRequested the number of bytes which are required
+     * @throws IOException if the stream ends before the required number of bytes are acquired
+     */
+    protected abstract void validateIndex(int index, int bytesRequested) throws IOException;
+
+    protected abstract boolean isValidIndex(int index, int bytesRequested) throws IOException;
+
+    /**
+     * Returns the length of the data source in bytes.
+     * <p>
+     * This is a simple operation for implementations (such as {@link RandomAccessFileReader} and
+     * {@link ByteArrayReader}) that have the entire data source available.
+     * <p>
+     * Users of this method must be aware that sequentially accessed implementations such as
+     * {@link RandomAccessStreamReader} will have to read and buffer the entire data source in
+     * order to determine the length.
+     *
+     * @return the length of the data source, in bytes.
+     */
+    public abstract long getLength() throws IOException;
+
+    /**
+     * Sets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     *
+     * @param motorolaByteOrder <code>true</code> for Motorola/big endian, <code>false</code> for Intel/little endian
+     */
+    public void setMotorolaByteOrder(boolean motorolaByteOrder)
+    {
+        _isMotorolaByteOrder = motorolaByteOrder;
+    }
+
+    /**
+     * Gets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     */
+    public boolean isMotorolaByteOrder()
+    {
+        return _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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public short getUInt8(int index) throws IOException
+    {
+        validateIndex(index, 1);
+
+        return (short) (getByte(index) & 0xFF);
+    }
+
+    /**
+     * 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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public byte getInt8(int index) throws IOException
+    {
+        validateIndex(index, 1);
+
+        return getByte(index);
+    }
+
+    /**
+     * 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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public int getUInt16(int index) throws IOException
+    {
+        validateIndex(index, 2);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (getByte(index    ) << 8 & 0xFF00) |
+                   (getByte(index + 1)      & 0xFF);
+        } else {
+            // Intel ordering - LSB first
+            return (getByte(index + 1) << 8 & 0xFF00) |
+                   (getByte(index    )      & 0xFF);
+        }
+    }
+
+    /**
+     * 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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public short getInt16(int index) throws IOException
+    {
+        validateIndex(index, 2);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (short) (((short)getByte(index    ) << 8 & (short)0xFF00) |
+                            ((short)getByte(index + 1)      & (short)0xFF));
+        } else {
+            // Intel ordering - LSB first
+            return (short) (((short)getByte(index + 1) << 8 & (short)0xFF00) |
+                            ((short)getByte(index    )      & (short)0xFF));
+        }
+    }
+
+    /**
+     * 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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public long getUInt32(int index) throws IOException
+    {
+        validateIndex(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (((long)getByte(index    )) << 24 & 0xFF000000L) |
+                   (((long)getByte(index + 1)) << 16 & 0xFF0000L) |
+                   (((long)getByte(index + 2)) << 8  & 0xFF00L) |
+                   (((long)getByte(index + 3))       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (((long)getByte(index + 3)) << 24 & 0xFF000000L) |
+                   (((long)getByte(index + 2)) << 16 & 0xFF0000L) |
+                   (((long)getByte(index + 1)) << 8  & 0xFF00L) |
+                   (((long)getByte(index    ))       & 0xFFL);
+        }
+    }
+
+    /**
+     * 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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public int getInt32(int index) throws IOException
+    {
+        validateIndex(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (getByte(index    ) << 24 & 0xFF000000) |
+                   (getByte(index + 1) << 16 & 0xFF0000) |
+                   (getByte(index + 2) << 8  & 0xFF00) |
+                   (getByte(index + 3)       & 0xFF);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (getByte(index + 3) << 24 & 0xFF000000) |
+                   (getByte(index + 2) << 16 & 0xFF0000) |
+                   (getByte(index + 1) << 8  & 0xFF00) |
+                   (getByte(index    )       & 0xFF);
+        }
+    }
+
+    /**
+     * 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 IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public long getInt64(int index) throws IOException
+    {
+        validateIndex(index, 8);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return ((long)getByte(index    ) << 56 & 0xFF00000000000000L) |
+                   ((long)getByte(index + 1) << 48 & 0xFF000000000000L) |
+                   ((long)getByte(index + 2) << 40 & 0xFF0000000000L) |
+                   ((long)getByte(index + 3) << 32 & 0xFF00000000L) |
+                   ((long)getByte(index + 4) << 24 & 0xFF000000L) |
+                   ((long)getByte(index + 5) << 16 & 0xFF0000L) |
+                   ((long)getByte(index + 6) << 8  & 0xFF00L) |
+                   ((long)getByte(index + 7)       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first
+            return ((long)getByte(index + 7) << 56 & 0xFF00000000000000L) |
+                   ((long)getByte(index + 6) << 48 & 0xFF000000000000L) |
+                   ((long)getByte(index + 5) << 40 & 0xFF0000000000L) |
+                   ((long)getByte(index + 4) << 32 & 0xFF00000000L) |
+                   ((long)getByte(index + 3) << 24 & 0xFF000000L) |
+                   ((long)getByte(index + 2) << 16 & 0xFF0000L) |
+                   ((long)getByte(index + 1) << 8  & 0xFF00L) |
+                   ((long)getByte(index    )       & 0xFFL);
+        }
+    }
+
+    /**
+     * Gets a s15.16 fixed point float from the buffer.
+     * <p>
+     * This particular fixed point encoding has one sign bit, 15 numerator bits and 16 denominator bits.
+     *
+     * @return the floating point value
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public float getS15Fixed16(int index) throws IOException
+    {
+        validateIndex(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            float res = (getByte(index    ) & 0xFF) << 8 |
+                        (getByte(index + 1) & 0xFF);
+            int d =     (getByte(index + 2) & 0xFF) << 8 |
+                        (getByte(index + 3) & 0xFF);
+            return (float)(res + d/65536.0);
+        } else {
+            // this particular branch is untested
+            float res = (getByte(index + 3) & 0xFF) << 8 |
+                        (getByte(index + 2) & 0xFF);
+            int d =     (getByte(index + 1) & 0xFF) << 8 |
+                        (getByte(index    ) & 0xFF);
+            return (float)(res + d/65536.0);
+        }
+    }
+
+    public float getFloat32(int index) throws IOException
+    {
+        return Float.intBitsToFloat(getInt32(index));
+    }
+
+    public double getDouble64(int index) throws IOException
+    {
+        return Double.longBitsToDouble(getInt64(index));
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested) throws IOException
+    {
+        return new String(getBytes(index, bytesRequested));
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested, String charset) throws IOException
+    {
+        byte[] bytes = getBytes(index, bytesRequested);
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return new String(bytes);
+        }
+    }
+
+    /**
+     * 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 IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public String getNullTerminatedString(int index, int maxLengthBytes) throws IOException
+    {
+        // NOTE currently only really suited to single-byte character strings
+
+        byte[] bytes = getBytes(index, maxLengthBytes);
+
+        // Count the number of non-null bytes
+        int length = 0;
+        while (length < bytes.length && bytes[length] != '\0')
+            length++;
+
+        return new String(bytes, 0, length);
+    }
+}
Index: /trunk/src/com/drew/lang/Rational.java
===================================================================
--- /trunk/src/com/drew/lang/Rational.java	(revision 8131)
+++ /trunk/src/com/drew/lang/Rational.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -29,7 +29,10 @@
 /**
  * Immutable class for holding a rational number without loss of precision.  Provides
- * a familiar representation via toString() in form <code>numerator/denominator</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
+ * a familiar representation via {@link Rational#toString} in form <code>numerator/denominator</code>.
+ *
+ * Note that any value with a numerator of zero will be treated as zero, even if the
+ * denominator is also zero.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class Rational extends java.lang.Number implements Serializable
@@ -61,7 +64,10 @@
      *         to type <code>double</code>.
      */
+    @Override
     public double doubleValue()
     {
-        return (double) _numerator / (double) _denominator;
+        return _numerator == 0
+            ? 0.0
+            : (double) _numerator / (double) _denominator;
     }
 
@@ -73,7 +79,10 @@
      *         to type <code>float</code>.
      */
+    @Override
     public float floatValue()
     {
-        return (float) _numerator / (float) _denominator;
+        return _numerator == 0
+            ? 0.0f
+            : (float) _numerator / (float) _denominator;
     }
 
@@ -81,9 +90,10 @@
      * Returns the value of the specified number as a <code>byte</code>.
      * This may involve rounding or truncation.  This implementation simply
-     * casts the result of <code>doubleValue()</code> to <code>byte</code>.
+     * casts the result of {@link Rational#doubleValue} to <code>byte</code>.
      *
      * @return the numeric value represented by this object after conversion
      *         to type <code>byte</code>.
      */
+    @Override
     public final byte byteValue()
     {
@@ -94,9 +104,10 @@
      * Returns the value of the specified number as an <code>int</code>.
      * This may involve rounding or truncation.  This implementation simply
-     * casts the result of <code>doubleValue()</code> to <code>int</code>.
+     * casts the result of {@link Rational#doubleValue} to <code>int</code>.
      *
      * @return the numeric value represented by this object after conversion
      *         to type <code>int</code>.
      */
+    @Override
     public final int intValue()
     {
@@ -107,9 +118,10 @@
      * Returns the value of the specified number as a <code>long</code>.
      * This may involve rounding or truncation.  This implementation simply
-     * casts the result of <code>doubleValue()</code> to <code>long</code>.
+     * casts the result of {@link Rational#doubleValue} to <code>long</code>.
      *
      * @return the numeric value represented by this object after conversion
      *         to type <code>long</code>.
      */
+    @Override
     public final long longValue()
     {
@@ -120,9 +132,10 @@
      * Returns the value of the specified number as a <code>short</code>.
      * This may involve rounding or truncation.  This implementation simply
-     * casts the result of <code>doubleValue()</code> to <code>short</code>.
+     * casts the result of {@link Rational#doubleValue} to <code>short</code>.
      *
      * @return the numeric value represented by this object after conversion
      *         to type <code>short</code>.
      */
+    @Override
     public final short shortValue()
     {
@@ -154,5 +167,5 @@
     }
 
-    /** Checks if this rational number is an Integer, either positive or negative. */
+    /** Checks if this {@link Rational} number is an Integer, either positive or negative. */
     public boolean isInteger()
     {
@@ -167,4 +180,5 @@
      * @return a string representation of the object.
      */
+    @Override
     @NotNull
     public String toString()
@@ -173,5 +187,5 @@
     }
 
-    /** Returns the simplest representation of this Rational's value possible. */
+    /** Returns the simplest representation of this {@link Rational}'s value possible. */
     @NotNull
     public String toSimpleString(boolean allowDecimal)
@@ -211,10 +225,10 @@
 
     /**
-     * Compares two <code>Rational</code> instances, returning true if they are mathematically
+     * Compares two {@link Rational} instances, returning true if they are mathematically
      * equivalent.
      *
-     * @param obj the Rational to compare this instance to.
+     * @param obj the {@link 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>.
+     *         return false if <code>obj</code> is not an instance of {@link Rational}.
      */
     @Override
@@ -235,5 +249,5 @@
     /**
      * <p>
-     * Simplifies the Rational number.</p>
+     * Simplifies the {@link Rational} number.</p>
      * <p>
      * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p>
@@ -244,5 +258,5 @@
      * <p>
      * However, generating the prime number series seems to be a hefty task.  Perhaps
-     * it's simpler to check if both d & n are divisible by all numbers from 2 ->
+     * it's simpler to check if both d &amp; n are divisible by all numbers from 2 {@literal ->}
      * (Math.min(denominator, numerator) / 2).  In doing this, one can check for 2
      * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5.
@@ -250,13 +264,13 @@
      * <p>
      * Therefore, the max number of pairs of modulus divisions required will be:</p>
-     * <code><pre>
+     * <pre><code>
      *    4   Math.min(denominator, numerator) - 1
      *   -- * ------------------------------------ + 2
      *   10                    2
-     * <p/>
+     *
      *   Math.min(denominator, numerator) - 1
      * = ------------------------------------ + 2
      *                  5
-     * </pre></code>
+     * </code></pre>
      *
      * @return a simplified instance, or if the Rational could not be simplified,
Index: /trunk/src/com/drew/lang/SequentialByteArrayReader.java
===================================================================
--- /trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 8132)
+++ /trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 8132)
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SequentialByteArrayReader extends SequentialReader
+{
+    @NotNull
+    private final byte[] _bytes;
+    private int _index;
+
+    @SuppressWarnings("ConstantConditions")
+    public SequentialByteArrayReader(@NotNull byte[] bytes)
+    {
+        if (bytes == null)
+            throw new NullPointerException();
+
+        _bytes = bytes;
+        _index = 0;
+    }
+
+    @Override
+    protected byte getByte() throws IOException
+    {
+        if (_index >= _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+        return _bytes[_index++];
+    }
+
+    @NotNull
+    @Override
+    public byte[] getBytes(int count) throws IOException
+    {
+        if (_index + count > _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+
+        byte[] bytes = new byte[count];
+        System.arraycopy(_bytes, _index, bytes, 0, count);
+        _index += count;
+
+        return bytes;
+    }
+
+    @Override
+    public void skip(long n) throws IOException
+    {
+        if (n < 0) {
+            throw new IllegalArgumentException("n must be zero or greater.");
+        }
+
+        if (_index + n > _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+
+        _index += n;
+    }
+
+    @Override
+    public boolean trySkip(long n) throws IOException
+    {
+        if (n < 0) {
+            throw new IllegalArgumentException("n must be zero or greater.");
+        }
+
+        _index += n;
+
+        if (_index > _bytes.length) {
+            _index = _bytes.length;
+            return false;
+        }
+
+        return true;
+    }
+}
Index: /trunk/src/com/drew/lang/SequentialReader.java
===================================================================
--- /trunk/src/com/drew/lang/SequentialReader.java	(revision 8132)
+++ /trunk/src/com/drew/lang/SequentialReader.java	(revision 8132)
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class SequentialReader
+{
+    // TODO review whether the masks are needed (in both this and RandomAccessReader)
+
+    private boolean _isMotorolaByteOrder = true;
+
+    /**
+     * Gets the next byte in the sequence.
+     *
+     * @return The read byte value
+     */
+    protected abstract byte getByte() throws IOException;
+
+    /**
+     * Returns the required number of bytes from the sequence.
+     *
+     * @param count The number of bytes to be returned
+     * @return The requested bytes
+     */
+    @NotNull
+    public abstract byte[] getBytes(int count) throws IOException;
+
+    /**
+     * Skips forward in the sequence. If the sequence ends, an {@link EOFException} is thrown.
+     *
+     * @param n the number of byte to skip. Must be zero or greater.
+     * @throws EOFException the end of the sequence is reached.
+     * @throws IOException an error occurred reading from the underlying source.
+     */
+    public abstract void skip(long n) throws IOException;
+
+    /**
+     * Skips forward in the sequence, returning a boolean indicating whether the skip succeeded, or whether the sequence ended.
+     *
+     * @param n the number of byte to skip. Must be zero or greater.
+     * @return a boolean indicating whether the skip succeeded, or whether the sequence ended.
+     * @throws IOException an error occurred reading from the underlying source.
+     */
+    public abstract boolean trySkip(long n) throws IOException;
+
+    /**
+     * Sets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     *
+     * @param motorolaByteOrder <code>true</code> for Motorola/big endian, <code>false</code> for Intel/little endian
+     */
+    public void setMotorolaByteOrder(boolean motorolaByteOrder)
+    {
+        _isMotorolaByteOrder = motorolaByteOrder;
+    }
+
+    /**
+     * Gets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     */
+    public boolean isMotorolaByteOrder()
+    {
+        return _isMotorolaByteOrder;
+    }
+
+    /**
+     * Returns an unsigned 8-bit int calculated from the next byte of the sequence.
+     *
+     * @return the 8 bit int value, between 0 and 255
+     */
+    public short getUInt8() throws IOException
+    {
+        return (short) (getByte() & 0xFF);
+    }
+
+    /**
+     * Returns a signed 8-bit int calculated from the next byte the sequence.
+     *
+     * @return the 8 bit int value, between 0x00 and 0xFF
+     */
+    public byte getInt8() throws IOException
+    {
+        return getByte();
+    }
+
+    /**
+     * Returns an unsigned 16-bit int calculated from the next two bytes of the sequence.
+     *
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     */
+    public int getUInt16() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (getByte() << 8 & 0xFF00) |
+                   (getByte()      & 0xFF);
+        } else {
+            // Intel ordering - LSB first
+            return (getByte()      & 0xFF) |
+                   (getByte() << 8 & 0xFF00);
+        }
+    }
+
+    /**
+     * Returns a signed 16-bit int calculated from two bytes of data (MSB, LSB).
+     *
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public short getInt16() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (short) (((short)getByte() << 8 & (short)0xFF00) |
+                            ((short)getByte()      & (short)0xFF));
+        } else {
+            // Intel ordering - LSB first
+            return (short) (((short)getByte()      & (short)0xFF) |
+                            ((short)getByte() << 8 & (short)0xFF00));
+        }
+    }
+
+    /**
+     * Get a 32-bit unsigned integer from the buffer, returning it as a long.
+     *
+     * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public long getUInt32() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (((long)getByte()) << 24 & 0xFF000000L) |
+                   (((long)getByte()) << 16 & 0xFF0000L) |
+                   (((long)getByte()) << 8  & 0xFF00L) |
+                   (((long)getByte())       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (((long)getByte())       & 0xFFL) |
+                   (((long)getByte()) << 8  & 0xFF00L) |
+                   (((long)getByte()) << 16 & 0xFF0000L) |
+                   (((long)getByte()) << 24 & 0xFF000000L);
+        }
+    }
+
+    /**
+     * Returns a signed 32-bit integer from four bytes of data.
+     *
+     * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public int getInt32() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (getByte() << 24 & 0xFF000000) |
+                   (getByte() << 16 & 0xFF0000) |
+                   (getByte() << 8  & 0xFF00) |
+                   (getByte()       & 0xFF);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (getByte()       & 0xFF) |
+                   (getByte() << 8  & 0xFF00) |
+                   (getByte() << 16 & 0xFF0000) |
+                   (getByte() << 24 & 0xFF000000);
+        }
+    }
+
+    /**
+     * Get a signed 64-bit integer from the buffer.
+     *
+     * @return the 64 bit int value, between 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public long getInt64() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return ((long)getByte() << 56 & 0xFF00000000000000L) |
+                   ((long)getByte() << 48 & 0xFF000000000000L) |
+                   ((long)getByte() << 40 & 0xFF0000000000L) |
+                   ((long)getByte() << 32 & 0xFF00000000L) |
+                   ((long)getByte() << 24 & 0xFF000000L) |
+                   ((long)getByte() << 16 & 0xFF0000L) |
+                   ((long)getByte() << 8  & 0xFF00L) |
+                   ((long)getByte()       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first
+            return ((long)getByte()       & 0xFFL) |
+                   ((long)getByte() << 8  & 0xFF00L) |
+                   ((long)getByte() << 16 & 0xFF0000L) |
+                   ((long)getByte() << 24 & 0xFF000000L) |
+                   ((long)getByte() << 32 & 0xFF00000000L) |
+                   ((long)getByte() << 40 & 0xFF0000000000L) |
+                   ((long)getByte() << 48 & 0xFF000000000000L) |
+                   ((long)getByte() << 56 & 0xFF00000000000000L);
+        }
+    }
+
+    /**
+     * Gets a s15.16 fixed point float from the buffer.
+     * <p>
+     * This particular fixed point encoding has one sign bit, 15 numerator bits and 16 denominator bits.
+     *
+     * @return the floating point value
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public float getS15Fixed16() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            float res = (getByte() & 0xFF) << 8 |
+                        (getByte() & 0xFF);
+            int d =     (getByte() & 0xFF) << 8 |
+                        (getByte() & 0xFF);
+            return (float)(res + d/65536.0);
+        } else {
+            // this particular branch is untested
+            int d =     (getByte() & 0xFF) |
+                        (getByte() & 0xFF) << 8;
+            float res = (getByte() & 0xFF) |
+                        (getByte() & 0xFF) << 8;
+            return (float)(res + d/65536.0);
+        }
+    }
+
+    public float getFloat32() throws IOException
+    {
+        return Float.intBitsToFloat(getInt32());
+    }
+
+    public double getDouble64() throws IOException
+    {
+        return Double.longBitsToDouble(getInt64());
+    }
+
+    @NotNull
+    public String getString(int bytesRequested) throws IOException
+    {
+        return new String(getBytes(bytesRequested));
+    }
+
+    @NotNull
+    public String getString(int bytesRequested, String charset) throws IOException
+    {
+        byte[] bytes = getBytes(bytesRequested);
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return new String(bytes);
+        }
+    }
+
+    /**
+     * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
+     *
+     * @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 IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public String getNullTerminatedString(int maxLengthBytes) throws IOException
+    {
+        // NOTE currently only really suited to single-byte character strings
+
+        byte[] bytes = new byte[maxLengthBytes];
+
+        // Count the number of non-null bytes
+        int length = 0;
+        while (length < bytes.length && (bytes[length] = getByte()) != '\0')
+            length++;
+
+        return new String(bytes, 0, length);
+    }
+}
Index: /trunk/src/com/drew/lang/StreamReader.java
===================================================================
--- /trunk/src/com/drew/lang/StreamReader.java	(revision 8132)
+++ /trunk/src/com/drew/lang/StreamReader.java	(revision 8132)
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class StreamReader extends SequentialReader
+{
+    @NotNull
+    private final InputStream _stream;
+
+    @SuppressWarnings("ConstantConditions")
+    public StreamReader(@NotNull InputStream stream)
+    {
+        if (stream == null)
+            throw new NullPointerException();
+
+        _stream = stream;
+    }
+
+    @Override
+    protected byte getByte() throws IOException
+    {
+        int value = _stream.read();
+        if (value == -1)
+            throw new EOFException("End of data reached.");
+        return (byte)value;
+    }
+
+    @NotNull
+    @Override
+    public byte[] getBytes(int count) throws IOException
+    {
+        byte[] bytes = new byte[count];
+        int totalBytesRead = 0;
+
+        while (totalBytesRead != count) {
+            final int bytesRead = _stream.read(bytes, totalBytesRead, count - totalBytesRead);
+            if (bytesRead == -1)
+                throw new EOFException("End of data reached.");
+            totalBytesRead += bytesRead;
+            assert(totalBytesRead <= count);
+        }
+
+        return bytes;
+    }
+
+    @Override
+    public void skip(long n) throws IOException
+    {
+        if (n < 0)
+            throw new IllegalArgumentException("n must be zero or greater.");
+
+        long skippedCount = skipInternal(n);
+
+        if (skippedCount != n)
+            throw new EOFException(String.format("Unable to skip. Requested %d bytes but skipped %d.", n, skippedCount));
+    }
+
+    @Override
+    public boolean trySkip(long n) throws IOException
+    {
+        if (n < 0)
+            throw new IllegalArgumentException("n must be zero or greater.");
+
+        return skipInternal(n) == n;
+    }
+
+    private long skipInternal(long n) throws IOException
+    {
+        // It seems that for some streams, such as BufferedInputStream, that skip can return
+        // some smaller number than was requested. So loop until we either skip enough, or
+        // InputStream.skip returns zero.
+        //
+        // See http://stackoverflow.com/questions/14057720/robust-skipping-of-data-in-a-java-io-inputstream-and-its-subtypes
+        //
+        long skippedTotal = 0;
+        while (skippedTotal != n) {
+            long skipped = _stream.skip(n - skippedTotal);
+            assert(skipped >= 0);
+            skippedTotal += skipped;
+            if (skipped == 0)
+                break;
+        }
+        return skippedTotal;
+    }
+}
Index: /trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- /trunk/src/com/drew/lang/StringUtil.java	(revision 8131)
+++ /trunk/src/com/drew/lang/StringUtil.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -23,10 +23,18 @@
 
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.util.Iterator;
 
-/** @author Drew Noakes http://drewnoakes.com */
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
 public class StringUtil
 {
+    @NotNull
     public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter)
     {
@@ -50,4 +58,5 @@
     }
 
+    @NotNull
     public static <T extends CharSequence> String join(@NotNull T[] strings, @NotNull String delimiter)
     {
@@ -69,3 +78,38 @@
         return buffer.toString();
     }
+
+    @NotNull
+    public static String fromStream(@NotNull InputStream stream) throws IOException
+    {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+        StringBuilder sb = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            sb.append(line);
+        }
+        return sb.toString();
+    }
+
+    public static int compare(@Nullable String s1, @Nullable String s2)
+    {
+        boolean null1 = s1 == null;
+        boolean null2 = s2 == null;
+
+        if (null1 && null2) {
+            return 0;
+        } else if (null1) {
+            return -1;
+        } else if (null2) {
+            return 1;
+        } else {
+            return s1.compareTo(s2);
+        }
+    }
+
+    @NotNull
+    public static String urlEncode(@NotNull String name)
+    {
+        // Sufficient for now, it seems
+        return name.replace(" ", "%20");
+    }
 }
Index: /trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- /trunk/src/com/drew/lang/annotations/NotNull.java	(revision 8131)
+++ /trunk/src/com/drew/lang/annotations/NotNull.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -23,5 +23,5 @@
 
 /**
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public @interface NotNull
Index: /trunk/src/com/drew/lang/annotations/Nullable.java
===================================================================
--- /trunk/src/com/drew/lang/annotations/Nullable.java	(revision 8131)
+++ /trunk/src/com/drew/lang/annotations/Nullable.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -23,5 +23,5 @@
 
 /**
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public @interface Nullable
Index: /trunk/src/com/drew/lang/annotations/SuppressWarnings.java
===================================================================
--- /trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 8131)
+++ /trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 8132)
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
Index: /trunk/src/com/drew/lang/annotations/package.html
===================================================================
--- /trunk/src/com/drew/lang/annotations/package.html	(revision 8132)
+++ /trunk/src/com/drew/lang/annotations/package.html	(revision 8132)
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA
+to provide design-time warnings about potential run-time errors.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/lang/package.html
===================================================================
--- /trunk/src/com/drew/lang/package.html	(revision 8132)
+++ /trunk/src/com/drew/lang/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes of generic utility.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/metadata/Age.java
===================================================================
--- /trunk/src/com/drew/metadata/Age.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/Age.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -27,21 +27,22 @@
 /**
  * Represents an age in years, months, days, hours, minutes and seconds.
- * <p/>
+ * <p>
  * Used by certain Panasonic cameras which have face recognition features.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class Age
 {
-    private int _years;
-    private int _months;
-    private int _days;
-    private int _hours;
-    private int _minutes;
-    private int _seconds;
+    private final int _years;
+    private final int _months;
+    private final int _days;
+    private final int _hours;
+    private final int _minutes;
+    private final 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
Index: /trunk/src/com/drew/metadata/DefaultTagDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/DefaultTagDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/DefaultTagDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
@@ -28,5 +28,5 @@
  * and gives descriptions using the default string representation of the value.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class DefaultTagDescriptor extends TagDescriptor<Directory>
Index: /trunk/src/com/drew/metadata/Directory.java
===================================================================
--- /trunk/src/com/drew/metadata/Directory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/Directory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
@@ -37,10 +37,8 @@
  * data types.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public abstract class Directory
 {
-    // TODO get Array methods need to return cloned data, to maintain this directory's integrity
-
     /** Map of values hashed by type identifiers. */
     @NotNull
@@ -104,5 +102,5 @@
     public Collection<Tag> getTags()
     {
-        return _definedTagList;
+        return Collections.unmodifiableCollection(_definedTagList);
     }
 
@@ -158,5 +156,5 @@
     public Iterable<String> getErrors()
     {
-        return _errorList;
+        return Collections.unmodifiableCollection(_errorList);
     }
 
@@ -414,5 +412,7 @@
             return null;
 
-        if (o instanceof String) {
+        if (o instanceof Number) {
+            return ((Number)o).intValue();
+        } else if (o instanceof String) {
             try {
                 return Integer.parseInt((String)o);
@@ -428,6 +428,4 @@
                 return (int)val;
             }
-        } else if (o instanceof Number) {
-            return ((Number)o).intValue();
         } else if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
@@ -498,4 +496,6 @@
         if (o == null)
             return null;
+        if (o instanceof int[])
+            return (int[])o;
         if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
@@ -506,12 +506,17 @@
             return ints;
         }
-        if (o instanceof int[])
-            return (int[])o;
+        if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            int[] ints = new int[shorts.length];
+            for (int i = 0; i < shorts.length; i++) {
+                ints[i] = shorts[i];
+            }
+            return ints;
+        }
         if (o instanceof byte[]) {
             byte[] bytes = (byte[])o;
             int[] ints = new int[bytes.length];
             for (int i = 0; i < bytes.length; i++) {
-                byte b = bytes[i];
-                ints[i] = b;
+                ints[i] = bytes[i];
             }
             return ints;
@@ -527,5 +532,5 @@
         if (o instanceof Integer)
             return new int[] { (Integer)o };
-        
+
         return null;
     }
@@ -560,4 +565,11 @@
             }
             return bytes;
+        } else if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            byte[] bytes = new byte[shorts.length];
+            for (int i = 0; i < shorts.length; i++) {
+                bytes[i] = (byte)shorts[i];
+            }
+            return bytes;
         } else if (o instanceof CharSequence) {
             CharSequence str = (CharSequence)o;
@@ -703,5 +715,5 @@
     /**
      * 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/>
+     * <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.
@@ -712,8 +724,8 @@
         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/>
+     * <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
@@ -818,4 +830,5 @@
             boolean isLongArray = componentType.getName().equals("long");
             boolean isByteArray = componentType.getName().equals("byte");
+            boolean isShortArray = componentType.getName().equals("short");
             StringBuilder string = new StringBuilder();
             for (int i = 0; i < arrayLength; i++) {
@@ -826,4 +839,6 @@
                 else if (isIntArray)
                     string.append(Array.getInt(o, i));
+                else if (isShortArray)
+                    string.append(Array.getShort(o, i));
                 else if (isLongArray)
                     string.append(Array.getLong(o, i));
@@ -896,4 +911,15 @@
 
     /**
+     * Gets whether the specified tag is known by the directory and has a name.
+     *
+     * @param tagType the tag type identifier
+     * @return whether this directory has a name for the specified tag
+     */
+    public boolean hasTagName(int tagType)
+    {
+        return getTagNameMap().containsKey(tagType);
+    }
+
+    /**
      * Provides a description of a tag's value using the descriptor set by
      * <code>setDescriptor(Descriptor)</code>.
@@ -908,3 +934,14 @@
         return _descriptor.getDescription(tagType);
     }
+
+    @Override
+    public String toString()
+    {
+        return String.format("%s Directory (%d %s)",
+            getName(),
+            _tagMap.size(),
+            _tagMap.size() == 1
+                ? "tag"
+                : "tags");
+    }
 }
Index: /trunk/src/com/drew/metadata/Face.java
===================================================================
--- /trunk/src/com/drew/metadata/Face.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/Face.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
@@ -26,5 +26,5 @@
 /**
  * Class to hold information about a detected or recognized face in a photo.
- * <p/>
+ * <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
@@ -116,4 +116,5 @@
     }
 
+    @Override
     @NotNull
     public String toString()
Index: /trunk/src/com/drew/metadata/Metadata.java
===================================================================
--- /trunk/src/com/drew/metadata/Metadata.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/Metadata.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
@@ -24,17 +24,13 @@
 import com.drew.lang.annotations.Nullable;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 
 /**
- * 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.
+ * A top-level object that holds the metadata values extracted from an image.
+ * <p>
+ * Metadata objects may contain zero or more {@link Directory} objects.  Each directory may contain zero or more tags
+ * with corresponding values.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public final class Metadata
@@ -42,5 +38,5 @@
     @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
@@ -58,5 +54,5 @@
     public Iterable<Directory> getDirectories()
     {
-        return _directoryList;
+        return Collections.unmodifiableCollection(_directoryList);
     }
 
@@ -72,7 +68,7 @@
 
     /**
-     * Returns a <code>Directory</code> of specified type.  If this <code>Metadata</code> object already contains
+     * Returns a {@link Directory} of specified type.  If this {@link Metadata} object already contains
      * such a directory, it is returned.  Otherwise a new instance of this directory will be created and stored within
-     * this Metadata object.
+     * this {@link Metadata} object.
      *
      * @param type the type of the Directory implementation required.
@@ -104,10 +100,10 @@
 
     /**
-     * If this <code>Metadata</code> object contains a <code>Directory</code> of the specified type, it is returned.
+     * If this {@link Metadata} object contains a {@link Directory} 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>.
+     * @return a Directory of type T if it exists in this {@link Metadata} object, otherwise <code>null</code>.
      */
     @Nullable
@@ -123,8 +119,8 @@
     /**
      * Indicates whether a given directory type has been created in this metadata
-     * repository.  Directories are created by calling <code>getOrCreateDirectory(Class)</code>.
+     * repository.  Directories are created by calling {@link Metadata#getOrCreateDirectory(Class)}.
      *
-     * @param type the Directory type
-     * @return true if the metadata directory has been created
+     * @param type the {@link Directory} type
+     * @return true if the {@link Directory} has been created
      */
     public boolean containsDirectory(Class<? extends Directory> type)
@@ -135,5 +131,5 @@
     /**
      * 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.
+     * This value will be true if Directory.hasErrors() is true for one of the contained {@link Directory} objects.
      *
      * @return whether one of the contained directories has an error
@@ -147,3 +143,13 @@
         return false;
     }
+
+    @Override
+    public String toString()
+    {
+        return String.format("Metadata (%d %s)",
+            _directoryList.size(),
+            _directoryList.size() == 1
+                ? "directory"
+                : "directories");
+    }
 }
Index: /trunk/src/com/drew/metadata/MetadataException.java
===================================================================
--- /trunk/src/com/drew/metadata/MetadataException.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/MetadataException.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
@@ -27,5 +27,5 @@
  * Base class for all metadata specific exceptions.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class MetadataException extends CompoundException
Index: /trunk/src/com/drew/metadata/MetadataReader.java
===================================================================
--- /trunk/src/com/drew/metadata/MetadataReader.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/MetadataReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,29 +16,27 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
 
-import com.drew.lang.BufferReader;
+import com.drew.lang.RandomAccessReader;
 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.
+ * Defines an object capable of processing a particular type of metadata from a {@link RandomAccessReader}.
+ * <p>
+ * Instances of this interface must be thread-safe and reusable.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public interface MetadataReader
 {
     /**
-     * Extract metadata from the source and merge it into an existing Metadata object.
+     * Extracts metadata from <code>reader</code> and merges it into the specified {@link 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.
+     * @param reader   The {@link RandomAccessReader} from which the metadata should be extracted.
+     * @param metadata The {@link Metadata} object into which extracted values should be merged.
      */
-    public void extract(@NotNull final BufferReader reader, @NotNull final Metadata metadata);
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata);
 }
Index: /trunk/src/com/drew/metadata/Tag.java
===================================================================
--- /trunk/src/com/drew/metadata/Tag.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/Tag.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
@@ -25,8 +25,8 @@
 
 /**
- * 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.
+ * Models a particular tag within a {@link com.drew.metadata.Directory} and provides methods for obtaining its value.
+ * Immutable.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class Tag
@@ -79,4 +79,18 @@
 
     /**
+     * Get whether this tag has a name.
+     *
+     * If <code>true</code>, it may be accessed via {@link #getTagName}.
+     * If <code>false</code>, {@link #getTagName} will return a string resembling <code>"Unknown tag (0x1234)"</code>.
+     *
+     * @return whether this tag has a name
+     */
+    @NotNull
+    public boolean hasTagName()
+    {
+        return _directory.hasTagName(_tagType);
+    }
+
+    /**
      * Get the name of the tag, such as <code>Aperture</code>, or
      * <code>InteropVersion</code>.
@@ -91,8 +105,8 @@
 
     /**
-     * Get the name of the directory in which the tag exists, such as
+     * Get the name of the {@link com.drew.metadata.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
+     * @return name of the {@link com.drew.metadata.Directory} in which this tag exists
      */
     @NotNull
@@ -107,9 +121,10 @@
      * @return the tag's type and value
      */
+    @Override
     @NotNull
     public String toString()
     {
         String description = getDescription();
-        if (description==null)
+        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 8131)
+++ /trunk/src/com/drew/metadata/TagDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,22 +16,28 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata;
 
+import com.drew.lang.Rational;
+import com.drew.lang.StringUtil;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 
+import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
 
 /**
- * Abstract base class for all tag descriptor classes.  Implementations are responsible for
+ * Base class for all tag descriptor classes.  Implementations are responsible for
  * 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
+ * @author Drew Noakes https://drewnoakes.com
  */
-public abstract class TagDescriptor<T extends Directory>
+public class TagDescriptor<T extends Directory>
 {
     @NotNull
@@ -44,9 +50,9 @@
 
     /**
-     * Returns a descriptive value of the the specified tag for this image.
+     * Returns a descriptive value of the specified tag for this image.
      * Where possible, known values will be substituted here in place of the raw
      * 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
@@ -58,5 +64,5 @@
         Object object = _directory.getObject(tagType);
 
-        if (object==null)
+        if (object == null)
             return null;
 
@@ -66,5 +72,5 @@
             if (length > 16) {
                 final String componentTypeName = object.getClass().getComponentType().getName();
-                return String.format("[%d %s%s]", length, componentTypeName, length==1 ? "" : "s");
+                return String.format("[%d %s%s]", length, componentTypeName, length == 1 ? "" : "s");
             }
         }
@@ -77,12 +83,13 @@
      * Takes a series of 4 bytes from the specified offset, and converts these to a
      * well-known version number, where possible.
-     * <p/>
+     * <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>
+     * <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 
+     *
+     * @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
      */
@@ -90,5 +97,5 @@
     public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits)
     {
-        if (components==null)
+        if (components == null)
             return null;
         StringBuilder version = new StringBuilder();
@@ -99,5 +106,5 @@
             if (c < '0')
                 c += '0';
-            if (i == 0 && c=='0')
+            if (i == 0 && c == '0')
                 continue;
             version.append(c);
@@ -105,3 +112,163 @@
         return version.toString();
     }
+
+    @Nullable
+    protected String getVersionBytesDescription(final int tagType, int majorDigits)
+    {
+        int[] values = _directory.getIntArray(tagType);
+        return values == null ? null : convertBytesToVersionString(values, majorDigits);
+    }
+
+    @Nullable
+    protected String getIndexedDescription(final int tagType, @NotNull String... descriptions)
+    {
+        return getIndexedDescription(tagType, 0, descriptions);
+    }
+
+    @Nullable
+    protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions)
+    {
+        final Integer index = _directory.getInteger(tagType);
+        if (index == null)
+            return null;
+        final int arrayIndex = index - baseIndex;
+        if (arrayIndex >= 0 && arrayIndex < descriptions.length) {
+            String description = descriptions[arrayIndex];
+            if (description != null)
+                return description;
+        }
+        return "Unknown (" + index + ")";
+    }
+
+    @Nullable
+    protected String getByteLengthDescription(final int tagType)
+    {
+        byte[] bytes = _directory.getByteArray(tagType);
+        if (bytes == null)
+            return null;
+        return String.format("(%d byte%s)", bytes.length, bytes.length == 1 ? "" : "s");
+    }
+
+    @Nullable
+    protected String getSimpleRational(final int tagType)
+    {
+        Rational value = _directory.getRational(tagType);
+        if (value == null)
+            return null;
+        return value.toSimpleString(true);
+    }
+
+    @Nullable
+    protected String getDecimalRational(final int tagType, final int decimalPlaces)
+    {
+        Rational value = _directory.getRational(tagType);
+        if (value == null)
+            return null;
+        return String.format("%." + decimalPlaces + "f", value.doubleValue());
+    }
+
+    @Nullable
+    protected String getFormattedInt(final int tagType, @NotNull final String format)
+    {
+        Integer value = _directory.getInteger(tagType);
+        if (value == null)
+            return null;
+        return String.format(format, value);
+    }
+
+    @Nullable
+    protected String getFormattedFloat(final int tagType, @NotNull final String format)
+    {
+        Float value = _directory.getFloatObject(tagType);
+        if (value == null)
+            return null;
+        return String.format(format, value);
+    }
+
+    @Nullable
+    protected String getFormattedString(final int tagType, @NotNull final String format)
+    {
+        String value = _directory.getString(tagType);
+        if (value == null)
+            return null;
+        return String.format(format, value);
+    }
+
+    @Nullable
+    protected String getEpochTimeDescription(final int tagType)
+    {
+        // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
+        Long value = _directory.getLongObject(tagType);
+        if (value==null)
+            return null;
+        return new Date(value).toString();
+    }
+
+    /**
+     * LSB first. Labels may be null, a String, or a String[2] with (low label,high label) values.
+     */
+    @Nullable
+    protected String getBitFlagDescription(final int tagType, @NotNull final Object... labels)
+    {
+        Integer value = _directory.getInteger(tagType);
+
+        if (value == null)
+            return null;
+
+        List<String> parts = new ArrayList<String>();
+
+        int bitIndex = 0;
+        while (labels.length > bitIndex) {
+            Object labelObj = labels[bitIndex];
+            if (labelObj != null) {
+                boolean isBitSet = (value & 1) == 1;
+                if (labelObj instanceof String[]) {
+                    String[] labelPair = (String[])labelObj;
+                    assert(labelPair.length == 2);
+                    parts.add(labelPair[isBitSet ? 1 : 0]);
+                } else if (isBitSet && labelObj instanceof String) {
+                    parts.add((String)labelObj);
+                }
+            }
+            value >>= 1;
+            bitIndex++;
+        }
+
+        return StringUtil.join(parts, ", ");
+    }
+
+    @Nullable
+    protected String get7BitStringFromBytes(final int tagType)
+    {
+        final byte[] bytes = _directory.getByteArray(tagType);
+
+        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
+    protected String getAsciiStringFromBytes(int tag)
+    {
+        byte[] values = _directory.getByteArray(tag);
+
+        if (values == null)
+            return null;
+
+        try {
+            return new String(values, "ASCII").trim();
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+    }
 }
Index: unk/src/com/drew/metadata/exif/CanonMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/CanonMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,833 +1,0 @@
-/*
- * 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>CanonMakernoteDirectory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory>
-{
-    public CanonMakernoteDescriptor(@NotNull CanonMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case CanonMakernoteDirectory.TAG_CANON_SERIAL_NUMBER:
-                return getSerialNumberDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FLASH_ACTIVITY:
-                return getFlashActivityDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_TYPE:
-                return getFocusTypeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_DIGITAL_ZOOM:
-                return getDigitalZoomDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_QUALITY:
-                return getQualityDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_MACRO_MODE:
-                return getMacroModeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_SELF_TIMER_DELAY:
-                return getSelfTimerDelayDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FLASH_MODE:
-                return getFlashModeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_CONTINUOUS_DRIVE_MODE:
-                return getContinuousDriveModeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_1:
-                return getFocusMode1Description();
-            case CanonMakernoteDirectory.CameraSettings.TAG_IMAGE_SIZE:
-                return getImageSizeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_EASY_SHOOTING_MODE:
-                return getEasyShootingModeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_CONTRAST:
-                return getContrastDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_SATURATION:
-                return getSaturationDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_SHARPNESS:
-                return getSharpnessDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_ISO:
-                return getIsoDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_METERING_MODE:
-                return getMeteringModeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_AF_POINT_SELECTED:
-                return getAfPointSelectedDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_EXPOSURE_MODE:
-                return getExposureModeDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_LONG_FOCAL_LENGTH:
-                return getLongFocalLengthDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_SHORT_FOCAL_LENGTH:
-                return getShortFocalLengthDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FOCAL_UNITS_PER_MM:
-                return getFocalUnitsPerMillimetreDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FLASH_DETAILS:
-                return getFlashDetailsDescription();
-            case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_2:
-                return getFocusMode2Description();
-            case CanonMakernoteDirectory.FocalLength.TAG_WHITE_BALANCE:
-                return getWhiteBalanceDescription();
-            case CanonMakernoteDirectory.FocalLength.TAG_AF_POINT_USED:
-                return getAfPointUsedDescription();
-            case CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS:
-                return getFlashBiasDescription();
-
-            // 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";
-            case 1:     return "On";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "AE lock/AF";
-            case 2:     return "AF/AF lock";
-            case 3:     return "AE+release/AE+AF";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "Enabled";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "1/3 stop";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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)";
-            case 1:     return "Off";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "1/200 (fixed)";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "0,-,+ / Disabled";
-            case 2:     return "-,0,+ / Enabled";
-            case 3:     return "-,0,+ / Disabled";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "2nd Curtain Sync";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "Operate AF";
-            case 2:     return "Lock AE and start timer";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "Disabled";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "Previous (volatile)";
-            case 2:     return "Previous";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "Change Quality";
-            case 2:     return "Change ISO Speed";
-            case 3:     return "Select Parameters";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 1:     return "Enabled";
-            default:    return "Unknown (" + value + ")";
-        }
-    }
-*/
-
-    @Nullable
-    public String getFlashBiasDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS);
-
-        if (value==null)
-            return null;
-
-        boolean isNegative = false;
-        if (value > 0xF000)
-        {
-            isNegative = true;
-            value = 0xFFFF - value;
-            value++;
-        }
-
-        // this tag is interesting in that the values returned are:
-        //  0, 0.375, 0.5, 0.626, 1
-        // not
-        //  0, 0.33,  0.5, 0.66,  1
-
-        return ((isNegative) ? "-" : "") + Float.toString(value / 32f) + " EV";
-    }
-
-    @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";
-        } else if ((value & 0x7) == 1) {
-            return "Centre";
-        } else if ((value & 0x7) == 2) {
-            return "Left";
-        } else {
-            return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getWhiteBalanceDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.FocalLength.TAG_WHITE_BALANCE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Auto";
-            case 1:
-                return "Sunny";
-            case 2:
-                return "Cloudy";
-            case 3:
-                return "Tungsten";
-            case 4:
-                return "Florescent";
-            case 5:
-                return "Flash";
-            case 6:
-                return "Custom";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFocusMode2Description()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_2);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Single";
-            case 1:
-                return "Continuous";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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) {
-            return "Internal flash";
-        }
-        if (((value >> 11) & 1) > 0) {
-            return "FP sync used";
-        }
-        if (((value >> 4) & 1) > 0) {
-            return "FP sync enabled";
-        }
-        return "Unknown (" + value + ")";
-    }
-
-    @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);
-        } else {
-            return "";
-        }
-    }
-
-    @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;
-    }
-
-    @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;
-    }
-
-    @Nullable
-    public String getExposureModeDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_EXPOSURE_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Easy shooting";
-            case 1:
-                return "Program";
-            case 2:
-                return "Tv-priority";
-            case 3:
-                return "Av-priority";
-            case 4:
-                return "Manual";
-            case 5:
-                return "A-DEP";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getAfPointSelectedDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_AF_POINT_SELECTED);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0x3000:
-                return "None (MF)";
-            case 0x3001:
-                return "Auto selected";
-            case 0x3002:
-                return "Right";
-            case 0x3003:
-                return "Centre";
-            case 0x3004:
-                return "Left";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getMeteringModeDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_METERING_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 3:
-                return "Evaluative";
-            case 4:
-                return "Partial";
-            case 5:
-                return "Centre weighted";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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:
-                return "Not specified (see ISOSpeedRatings tag)";
-            case 15:
-                return "Auto";
-            case 16:
-                return "50";
-            case 17:
-                return "100";
-            case 18:
-                return "200";
-            case 19:
-                return "400";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSharpnessDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SHARPNESS);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0xFFFF:
-                return "Low";
-            case 0x000:
-                return "Normal";
-            case 0x001:
-                return "High";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSaturationDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SATURATION);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0xFFFF:
-                return "Low";
-            case 0x000:
-                return "Normal";
-            case 0x001:
-                return "High";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getContrastDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_CONTRAST);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0xFFFF:
-                return "Low";
-            case 0x000:
-                return "Normal";
-            case 0x001:
-                return "High";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getEasyShootingModeDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_EASY_SHOOTING_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Full auto";
-            case 1:
-                return "Manual";
-            case 2:
-                return "Landscape";
-            case 3:
-                return "Fast shutter";
-            case 4:
-                return "Slow shutter";
-            case 5:
-                return "Night";
-            case 6:
-                return "B&W";
-            case 7:
-                return "Sepia";
-            case 8:
-                return "Portrait";
-            case 9:
-                return "Sports";
-            case 10:
-                return "Macro / Closeup";
-            case 11:
-                return "Pan focus";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getImageSizeDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_IMAGE_SIZE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Large";
-            case 1:
-                return "Medium";
-            case 2:
-                return "Small";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFocusMode1Description()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_1);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "One-shot";
-            case 1:
-                return "AI Servo";
-            case 2:
-                return "AI Focus";
-            case 3:
-                return "Manual Focus";
-            case 4:
-                // TODO should check field 32 here (FOCUS_MODE_2)
-                return "Single";
-            case 5:
-                return "Continuous";
-            case 6:
-                return "Manual Focus";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-        }
-        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:
-                return "No flash fired";
-            case 1:
-                return "Auto";
-            case 2:
-                return "On";
-            case 3:
-                return "Red-eye reduction";
-            case 4:
-                return "Slow-synchro";
-            case 5:
-                return "Auto and red-eye reduction";
-            case 6:
-                return "On and red-eye reduction";
-            case 16:
-                // note: this value not set on Canon D30
-                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";
-        } else {
-            // TODO find an image that tests this calculation
-            return Double.toString((double)value * 0.1d) + " sec";
-        }
-    }
-
-    @Nullable
-    public String getMacroModeDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_MACRO_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "Macro";
-            case 2:
-                return "Normal";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getQualityDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_QUALITY);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 2:
-                return "Normal";
-            case 3:
-                return "Fine";
-            case 5:
-                return "Superfine";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getDigitalZoomDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_DIGITAL_ZOOM);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "No digital zoom";
-            case 1:
-                return "2x";
-            case 2:
-                return "4x";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFocusTypeDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_TYPE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Manual";
-            case 1:
-                return "Auto";
-            case 3:
-                return "Close-up (Macro)";
-            case 8:
-                return "Locked (Pan Mode)";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFlashActivityDescription()
-    {
-        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FLASH_ACTIVITY);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Flash did not fire";
-            case 1:
-                return "Flash fired";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-}
Index: unk/src/com/drew/metadata/exif/CanonMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/CanonMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,704 +1,0 @@
-/*
- * 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 Canon cameras.
- *
- * Thanks to Bill Richards for his contribution to this makernote directory.
- *
- * 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
-{
-    // 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
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        _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");
-    }
-
-    public CanonMakernoteDirectory()
-    {
-        this.setDescriptor(new CanonMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Canon Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-
-    @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: unk/src/com/drew/metadata/exif/CasioType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/CasioType1MakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,328 +1,0 @@
-/*
- * 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>CasioType1MakernoteDirectory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory>
-{
-    public CasioType1MakernoteDescriptor(@NotNull CasioType1MakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case CasioType1MakernoteDirectory.TAG_CASIO_RECORDING_MODE:
-                return getRecordingModeDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_QUALITY:
-                return getQualityDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_FOCUSING_MODE:
-                return getFocusingModeDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_FLASH_MODE:
-                return getFlashModeDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_FLASH_INTENSITY:
-                return getFlashIntensityDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_OBJECT_DISTANCE:
-                return getObjectDistanceDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_WHITE_BALANCE:
-                return getWhiteBalanceDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_DIGITAL_ZOOM:
-                return getDigitalZoomDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_SHARPNESS:
-                return getSharpnessDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_CONTRAST:
-                return getContrastDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_SATURATION:
-                return getSaturationDescription();
-            case CasioType1MakernoteDirectory.TAG_CASIO_CCD_SENSITIVITY:
-                return getCcdSensitivityDescription();
-            default:
-                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
-            case 64:
-                return "Normal";
-            case 125:
-                return "+1.0";
-            case 250:
-                return "+2.0";
-            case 244:
-                return "+3.0";
-                // these two for QV8000/2000
-            case 80:
-                return "Normal (ISO 80 equivalent)";
-            case 100:
-                return "High";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSaturationDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_SATURATION);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 0:
-                return "Normal";
-            case 1:
-                return "Low";
-            case 2:
-                return "High";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getContrastDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_CONTRAST);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 0:
-                return "Normal";
-            case 1:
-                return "Low";
-            case 2:
-                return "High";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSharpnessDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_SHARPNESS);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 0:
-                return "Normal";
-            case 1:
-                return "Soft";
-            case 2:
-                return "Hard";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getDigitalZoomDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_DIGITAL_ZOOM);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 0x10000:
-                return "No digital zoom";
-            case 0x10001:
-                return "2x digital zoom";
-            case 0x20000:
-                return "2x digital zoom";
-            case 0x40000:
-                return "4x digital zoom";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getWhiteBalanceDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_WHITE_BALANCE);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 1:
-                return "Auto";
-            case 2:
-                return "Tungsten";
-            case 3:
-                return "Daylight";
-            case 4:
-                return "Florescent";
-            case 5:
-                return "Shade";
-            case 129:
-                return "Manual";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getObjectDistanceDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_OBJECT_DISTANCE);
-
-        if (value == null)
-            return null;
-
-        return value + " mm";
-    }
-
-    @Nullable
-    public String getFlashIntensityDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_INTENSITY);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 11:
-                return "Weak";
-            case 13:
-                return "Normal";
-            case 15:
-                return "Strong";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFlashModeDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_MODE);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 1:
-                return "Auto";
-            case 2:
-                return "On";
-            case 3:
-                return "Off";
-            case 4:
-                // this documented as additional value for off here:
-                // http://www.ozhiker.com/electronics/pjmt/jpeg_info/casio_mn.html
-                return "Red eye reduction";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFocusingModeDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_FOCUSING_MODE);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 2:
-                return "Macro";
-            case 3:
-                return "Auto focus";
-            case 4:
-                return "Manual focus";
-            case 5:
-                return "Infinity";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getQualityDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_QUALITY);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 1:
-                return "Economy";
-            case 2:
-                return "Normal";
-            case 3:
-                return "Fine";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getRecordingModeDescription()
-    {
-        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_RECORDING_MODE);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 1:
-                return "Single shutter";
-            case 2:
-                return "Panorama";
-            case 3:
-                return "Night scene";
-            case 4:
-                return "Portrait";
-            case 5:
-                return "Landscape";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-}
Index: unk/src/com/drew/metadata/exif/CasioType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/CasioType1MakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,102 +1,0 @@
-/*
- * 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 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
-{
-    public static final int TAG_CASIO_RECORDING_MODE = 0x0001;
-    public static final int TAG_CASIO_QUALITY = 0x0002;
-    public static final int TAG_CASIO_FOCUSING_MODE = 0x0003;
-    public static final int TAG_CASIO_FLASH_MODE = 0x0004;
-    public static final int TAG_CASIO_FLASH_INTENSITY = 0x0005;
-    public static final int TAG_CASIO_OBJECT_DISTANCE = 0x0006;
-    public static final int TAG_CASIO_WHITE_BALANCE = 0x0007;
-    public static final int TAG_CASIO_UNKNOWN_1 = 0x0008;
-    public static final int TAG_CASIO_UNKNOWN_2 = 0x0009;
-    public static final int TAG_CASIO_DIGITAL_ZOOM = 0x000A;
-    public static final int TAG_CASIO_SHARPNESS = 0x000B;
-    public static final int TAG_CASIO_CONTRAST = 0x000C;
-    public static final int TAG_CASIO_SATURATION = 0x000D;
-    public static final int TAG_CASIO_UNKNOWN_3 = 0x000E;
-    public static final int TAG_CASIO_UNKNOWN_4 = 0x000F;
-    public static final int TAG_CASIO_UNKNOWN_5 = 0x0010;
-    public static final int TAG_CASIO_UNKNOWN_6 = 0x0011;
-    public static final int TAG_CASIO_UNKNOWN_7 = 0x0012;
-    public static final int TAG_CASIO_UNKNOWN_8 = 0x0013;
-    public static final int TAG_CASIO_CCD_SENSITIVITY = 0x0014;
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        _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");
-    }
-
-    public CasioType1MakernoteDirectory()
-    {
-        this.setDescriptor(new CasioType1MakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Casio Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/CasioType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/CasioType2MakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,483 +1,0 @@
-/*
- * 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>CasioType2MakernoteDirectory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory>
-{
-    public CasioType2MakernoteDescriptor(@NotNull CasioType2MakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS:
-                return getThumbnailDimensionsDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_SIZE:
-                return getThumbnailSizeDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_OFFSET:
-                return getThumbnailOffsetDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY_MODE:
-                return getQualityModeDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_IMAGE_SIZE:
-                return getImageSizeDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_1:
-                return getFocusMode1Description();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ISO_SENSITIVITY:
-                return getIsoSensitivityDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_1:
-                return getWhiteBalance1Description();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCAL_LENGTH:
-                return getFocalLengthDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SATURATION:
-                return getSaturationDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CONTRAST:
-                return getContrastDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SHARPNESS:
-                return getSharpnessDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CASIO_PREVIEW_THUMBNAIL:
-                return getCasioPreviewThumbnailDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS:
-                return getWhiteBalanceBiasDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_2:
-                return getWhiteBalance2Description();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_OBJECT_DISTANCE:
-                return getObjectDistanceDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FLASH_DISTANCE:
-                return getFlashDistanceDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_RECORD_MODE:
-                return getRecordModeDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SELF_TIMER:
-                return getSelfTimerDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY:
-                return getQualityDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_2:
-                return getFocusMode2Description();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_TIME_ZONE:
-                return getTimeZoneDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_BESTSHOT_MODE:
-                return getBestShotModeDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY:
-                return getCcdIsoSensitivityDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_COLOUR_MODE:
-                return getColourModeDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ENHANCEMENT:
-                return getEnhancementDescription();
-            case CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FILTER:
-                return getFilterDescription();
-            default:
-                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:
-                return "Off";
-            case 1:
-                return "On";
-            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()
-    {
-        return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_TIME_ZONE);
-    }
-
-    @Nullable
-    public String getFocusMode2Description()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_2);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "Fixation";
-            case 6:
-                return "Multi-Area Focus";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getQualityDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 3:
-                return "Fine";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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:
-                return "Normal";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-    }
-
-    @Nullable
-    public String getWhiteBalance2Description()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_2);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Manual";
-            case 1:
-                return "Auto"; // unsure about this
-            case 4:
-                return "Flash"; // unsure about this
-            case 12:
-                return "Flash";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getWhiteBalanceBiasDescription()
-    {
-        return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS);
-    }
-
-    @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
-        return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO);
-    }
-
-    @Nullable
-    public String getSharpnessDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SHARPNESS);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "-1";
-            case 1:
-                return "Normal";
-            case 2:
-                return "+1";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getContrastDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CONTRAST);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "-1";
-            case 1:
-                return "Normal";
-            case 2:
-                return "+1";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSaturationDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SATURATION);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "-1";
-            case 1:
-                return "Normal";
-            case 2:
-                return "+1";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-    }
-
-    @Nullable
-    public String getWhiteBalance1Description()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_1);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Auto";
-            case 1:
-                return "Daylight";
-            case 2:
-                return "Shade";
-            case 3:
-                return "Tungsten";
-            case 4:
-                return "Florescent";
-            case 5:
-                return "Manual";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getIsoSensitivityDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ISO_SENSITIVITY);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 3:
-                return "50";
-            case 4:
-                return "64";
-            case 6:
-                return "100";
-            case 9:
-                return "200";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFocusMode1Description()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_1);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Normal";
-            case 1:
-                return "Macro";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-            case 4:  return "1600 x 1200 pixels";
-            case 5:  return "2048 x 1536 pixels";
-            case 20: return "2288 x 1712 pixels";
-            case 21: return "2592 x 1944 pixels";
-            case 22: return "2304 x 1728 pixels";
-            case 36: return "3008 x 2008 pixels";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getQualityModeDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "Fine";
-            case 2:
-                return "Super Fine";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getThumbnailOffsetDescription()
-    {
-        return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_OFFSET);
-    }
-
-    @Nullable
-    public String getThumbnailSizeDescription()
-    {
-        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_SIZE);
-        if (value==null)
-            return null;
-        return Integer.toString(value) + " bytes";
-    }
-
-    @Nullable
-    public String getThumbnailDimensionsDescription()
-    {
-        int[] dimensions = _directory.getIntArray(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS);
-        if (dimensions==null || dimensions.length!=2)
-            return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS);
-        return dimensions[0] + " x " + dimensions[1] + " pixels";
-    }
-}
Index: unk/src/com/drew/metadata/exif/CasioType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/CasioType2MakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,230 +1,0 @@
-/*
- * 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 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
-{
-    /**
-     * 2 values - x,y dimensions in pixels.
-     */
-    public static final int TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS = 0x0002;
-    /**
-     * Size in bytes
-     */
-    public static final int TAG_CASIO_TYPE2_THUMBNAIL_SIZE = 0x0003;
-    /**
-     * Offset of Preview Thumbnail
-     */
-    public static final int TAG_CASIO_TYPE2_THUMBNAIL_OFFSET = 0x0004;
-    /**
-     * 1 = Fine
-     * 2 = Super Fine
-     */
-    public static final int TAG_CASIO_TYPE2_QUALITY_MODE = 0x0008;
-    /**
-     * 0 = 640 x 480 pixels
-     * 4 = 1600 x 1200 pixels
-     * 5 = 2048 x 1536 pixels
-     * 20 = 2288 x 1712 pixels
-     * 21 = 2592 x 1944 pixels
-     * 22 = 2304 x 1728 pixels
-     * 36 = 3008 x 2008 pixels
-     */
-    public static final int TAG_CASIO_TYPE2_IMAGE_SIZE = 0x0009;
-    /**
-     * 0 = Normal
-     * 1 = Macro
-     */
-    public static final int TAG_CASIO_TYPE2_FOCUS_MODE_1 = 0x000D;
-    /**
-     * 3 = 50
-     * 4 = 64
-     * 6 = 100
-     * 9 = 200
-     */
-    public static final int TAG_CASIO_TYPE2_ISO_SENSITIVITY = 0x0014;
-    /**
-     * 0 = Auto
-     * 1 = Daylight
-     * 2 = Shade
-     * 3 = Tungsten
-     * 4 = Fluorescent
-     * 5 = Manual
-     */
-    public static final int TAG_CASIO_TYPE2_WHITE_BALANCE_1 = 0x0019;
-    /**
-     * Units are tenths of a millimetre
-     */
-    public static final int TAG_CASIO_TYPE2_FOCAL_LENGTH = 0x001D;
-    /**
-     * 0 = -1
-     * 1 = Normal
-     * 2 = +1
-     */
-    public static final int TAG_CASIO_TYPE2_SATURATION = 0x001F;
-    /**
-     * 0 = -1
-     * 1 = Normal
-     * 2 = +1
-     */
-    public static final int TAG_CASIO_TYPE2_CONTRAST = 0x0020;
-    /**
-     * 0 = -1
-     * 1 = Normal
-     * 2 = +1
-     */
-    public static final int TAG_CASIO_TYPE2_SHARPNESS = 0x0021;
-    /**
-     * See PIM specification here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-     */
-    public static final int TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
-    /**
-     * Alternate thumbnail offset
-     */
-    public static final int TAG_CASIO_TYPE2_CASIO_PREVIEW_THUMBNAIL = 0x2000;
-    /**
-     *
-     */
-    public static final int TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS = 0x2011;
-    /**
-     * 12 = Flash
-     * 0 = Manual
-     * 1 = Auto?
-     * 4 = Flash?
-     */
-    public static final int TAG_CASIO_TYPE2_WHITE_BALANCE_2 = 0x2012;
-    /**
-     * Units are millimetres
-     */
-    public static final int TAG_CASIO_TYPE2_OBJECT_DISTANCE = 0x2022;
-    /**
-     * 0 = Off
-     */
-    public static final int TAG_CASIO_TYPE2_FLASH_DISTANCE = 0x2034;
-    /**
-     * 2 = Normal Mode
-     */
-    public static final int TAG_CASIO_TYPE2_RECORD_MODE = 0x3000;
-    /**
-     * 1 = Off?
-     */
-    public static final int TAG_CASIO_TYPE2_SELF_TIMER = 0x3001;
-    /**
-     * 3 = Fine
-     */
-    public static final int TAG_CASIO_TYPE2_QUALITY = 0x3002;
-    /**
-     * 1 = Fixation
-     * 6 = Multi-Area Auto Focus
-     */
-    public static final int TAG_CASIO_TYPE2_FOCUS_MODE_2 = 0x3003;
-    /**
-     * (string)
-     */
-    public static final int TAG_CASIO_TYPE2_TIME_ZONE = 0x3006;
-    /**
-     *
-     */
-    public static final int TAG_CASIO_TYPE2_BESTSHOT_MODE = 0x3007;
-    /**
-     * 0 = Off
-     * 1 = On?
-     */
-    public static final int TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY = 0x3014;
-    /**
-     * 0 = Off
-     */
-    public static final int TAG_CASIO_TYPE2_COLOUR_MODE = 0x3015;
-    /**
-     * 0 = Off
-     */
-    public static final int TAG_CASIO_TYPE2_ENHANCEMENT = 0x3016;
-    /**
-     * 0 = Off
-     */
-    public static final int TAG_CASIO_TYPE2_FILTER = 0x3017;
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        // 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");
-    }
-
-    public CasioType2MakernoteDirectory()
-    {
-        this.setDescriptor(new CasioType2MakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Casio Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/DataFormat.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/DataFormat.java	(revision 8131)
+++ 	(revision )
@@ -1,87 +1,0 @@
-/*
- * 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.MetadataException;
-
-/**
- * An enumeration of data formats used in the TIFF IFDs.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class DataFormat
-{
-    @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);
-
-    @NotNull private final String _name;
-    private final int _value;
-
-    @NotNull
-    public static DataFormat fromValue(int value) throws MetadataException
-    {
-        switch (value)
-        {
-            case 1:  return BYTE;
-            case 2:  return STRING;
-            case 3:  return USHORT;
-            case 4:  return ULONG;
-            case 5:  return URATIONAL;
-            case 6:  return SBYTE;
-            case 7:  return UNDEFINED;
-            case 8:  return SSHORT;
-            case 9:  return SLONG;
-            case 10: return SRATIONAL;
-            case 11: return SINGLE;
-            case 12: return DOUBLE;
-        }
-
-        throw new MetadataException("value '"+value+"' does not represent a known data format.");
-    }
-
-    private DataFormat(@NotNull String name, int value)
-    {
-        _name = name;
-        _value = value;
-    }
-
-    public int getValue()
-    {
-        return _value;
-    }
-
-    @NotNull
-    public String toString()
-    {
-        return _name;
-    }
-}
Index: /trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -29,8 +29,10 @@
 import java.io.UnsupportedEncodingException;
 
+import static com.drew.metadata.exif.ExifIFD0Directory.*;
+
 /**
- * Provides human-readable string representations of tag values stored in a <code>ExifIFD0Directory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
+ * Provides human-readable string representations of tag values stored in a {@link ExifIFD0Directory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory>
@@ -54,5 +56,5 @@
 
     /**
-     * Returns a descriptive value of the the specified tag for this image.
+     * Returns a descriptive value of 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
@@ -62,30 +64,31 @@
      *         <code>null</code> if the tag hasn't been defined.
      */
+    @Override
     @Nullable
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case ExifIFD0Directory.TAG_RESOLUTION_UNIT:
+            case TAG_RESOLUTION_UNIT:
                 return getResolutionDescription();
-            case ExifIFD0Directory.TAG_YCBCR_POSITIONING:
+            case TAG_YCBCR_POSITIONING:
                 return getYCbCrPositioningDescription();
-            case ExifIFD0Directory.TAG_X_RESOLUTION:
+            case TAG_X_RESOLUTION:
                 return getXResolutionDescription();
-            case ExifIFD0Directory.TAG_Y_RESOLUTION:
+            case TAG_Y_RESOLUTION:
                 return getYResolutionDescription();
-            case ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE:
+            case TAG_REFERENCE_BLACK_WHITE:
                 return getReferenceBlackWhiteDescription();
-            case ExifIFD0Directory.TAG_ORIENTATION:
+            case TAG_ORIENTATION:
                 return getOrientationDescription();
 
-            case ExifIFD0Directory.TAG_WIN_AUTHOR:
+            case TAG_WIN_AUTHOR:
                return getWindowsAuthorDescription();
-            case ExifIFD0Directory.TAG_WIN_COMMENT:
+            case TAG_WIN_COMMENT:
                return getWindowsCommentDescription();
-            case ExifIFD0Directory.TAG_WIN_KEYWORDS:
+            case TAG_WIN_KEYWORDS:
                return getWindowsKeywordsDescription();
-            case ExifIFD0Directory.TAG_WIN_SUBJECT:
+            case TAG_WIN_SUBJECT:
                return getWindowsSubjectDescription();
-            case ExifIFD0Directory.TAG_WIN_TITLE:
+            case TAG_WIN_TITLE:
                return getWindowsTitleDescription();
 
@@ -98,6 +101,6 @@
     public String getReferenceBlackWhiteDescription()
     {
-        int[] ints = _directory.getIntArray(ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE);
-        if (ints==null)
+        int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
+        if (ints==null || ints.length < 6)
             return null;
         int blackR = ints[0];
@@ -107,6 +110,5 @@
         int blackB = ints[4];
         int whiteB = ints[5];
-        return "[" + blackR + "," + blackG + "," + blackB + "] " +
-               "[" + whiteR + "," + whiteG + "," + whiteB + "]";
+        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
     }
 
@@ -114,11 +116,11 @@
     public String getYResolutionDescription()
     {
-        Rational value = _directory.getRational(ExifIFD0Directory.TAG_Y_RESOLUTION);
+        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
         if (value==null)
             return null;
         final String unit = getResolutionDescription();
-        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
-                " dots per " +
-                (unit==null ? "unit" : unit.toLowerCase());
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
     }
 
@@ -126,11 +128,11 @@
     public String getXResolutionDescription()
     {
-        Rational value = _directory.getRational(ExifIFD0Directory.TAG_X_RESOLUTION);
+        Rational value = _directory.getRational(TAG_X_RESOLUTION);
         if (value==null)
             return null;
         final String unit = getResolutionDescription();
-        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
-                " dots per " +
-                (unit==null ? "unit" : unit.toLowerCase());
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
     }
 
@@ -138,71 +140,47 @@
     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);
+        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
+    }
+
+    @Nullable
+    public String getOrientationDescription()
+    {
+        return getIndexedDescription(TAG_ORIENTATION, 1,
+            "Top, left side (Horizontal / normal)",
+            "Top, right side (Mirror horizontal)",
+            "Bottom, right side (Rotate 180)",
+            "Bottom, left side (Mirror vertical)",
+            "Left side, top (Mirror horizontal and rotate 270 CW)",
+            "Right side, top (Rotate 90 CW)",
+            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
+            "Left side, bottom (Rotate 270 CW)");
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
+    }
+
+    /** The Windows specific tags uses plain Unicode. */
+    @Nullable
+    private String getUnicodeDescription(int tag)
+    {
+        byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null)
+            return null;
+        try {
+            // Decode the unicode string and trim the unicode zero "\0" from the end.
+            return new String(bytes, "UTF-16LE").trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
         }
     }
 
     @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);
+       return getUnicodeDescription(TAG_WIN_AUTHOR);
     }
 
@@ -210,5 +188,5 @@
     public String getWindowsCommentDescription()
     {
-       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_COMMENT);
+       return getUnicodeDescription(TAG_WIN_COMMENT);
     }
 
@@ -216,5 +194,5 @@
     public String getWindowsKeywordsDescription()
     {
-       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_KEYWORDS);
+       return getUnicodeDescription(TAG_WIN_KEYWORDS);
     }
 
@@ -222,5 +200,5 @@
     public String getWindowsTitleDescription()
     {
-       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_TITLE);
+       return getUnicodeDescription(TAG_WIN_TITLE);
     }
 
@@ -228,5 +206,5 @@
     public String getWindowsSubjectDescription()
     {
-       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_SUBJECT);
+       return getUnicodeDescription(TAG_WIN_SUBJECT);
     }
 }
Index: /trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -30,5 +30,5 @@
  * Describes Exif tags from the IFD0 directory.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifIFD0Directory extends Directory
@@ -46,8 +46,20 @@
     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;
+
+
+    /** 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 GPS IFD. */
+    public static final int TAG_GPS_INFO_OFFSET = 0x8825;
+
     public static final int TAG_COPYRIGHT = 0x8298;
+
+    /** Non-standard, but in use. */
+    public static final int TAG_TIME_ZONE_OFFSET = 0x882a;
 
     /** The image title, as used by Windows XP. */
@@ -82,5 +94,8 @@
         _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
         _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+
         _tagNameMap.put(TAG_COPYRIGHT, "Copyright");
+
+        _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
 
         _tagNameMap.put(TAG_WIN_AUTHOR, "Windows XP Author");
@@ -96,4 +111,5 @@
     }
 
+    @Override
     @NotNull
     public String getName()
@@ -102,4 +118,5 @@
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
Index: /trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
@@ -25,8 +25,10 @@
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.exif.ExifInteropDirectory.*;
+
 /**
- * Provides human-readable string representations of tag values stored in a <code>ExifInteropDirectory</code>.
+ * Provides human-readable string representations of tag values stored in a {@link ExifInteropDirectory}.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory>
@@ -37,11 +39,12 @@
     }
 
+    @Override
     @Nullable
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case ExifInteropDirectory.TAG_INTEROP_INDEX:
+            case TAG_INTEROP_INDEX:
                 return getInteropIndexDescription();
-            case ExifInteropDirectory.TAG_INTEROP_VERSION:
+            case TAG_INTEROP_VERSION:
                 return getInteropVersionDescription();
             default:
@@ -53,6 +56,5 @@
     public String getInteropVersionDescription()
     {
-        int[] ints = _directory.getIntArray(ExifInteropDirectory.TAG_INTEROP_VERSION);
-        return convertBytesToVersionString(ints, 2);
+        return getVersionBytesDescription(TAG_INTEROP_VERSION, 2);
     }
 
@@ -60,7 +62,7 @@
     public String getInteropIndexDescription()
     {
-        String value = _directory.getString(ExifInteropDirectory.TAG_INTEROP_INDEX);
+        String value = _directory.getString(TAG_INTEROP_INDEX);
 
-        if (value==null)
+        if (value == null)
             return null;
 
Index: /trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
@@ -29,5 +29,5 @@
  * Describes Exif interoperability tags.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifInteropDirectory extends Directory
@@ -56,4 +56,5 @@
     }
 
+    @Override
     @NotNull
     public String getName()
@@ -62,4 +63,5 @@
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
Index: /trunk/src/com/drew/metadata/exif/ExifReader.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,577 +16,101 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
 
-import java.util.HashSet;
-import java.util.Set;
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.imaging.tiff.TiffProcessingException;
+import com.drew.imaging.tiff.TiffReader;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
 
-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.IOException;
+import java.util.Arrays;
 
 /**
  * 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.
+ * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera
+ * makernote directories.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifReader implements MetadataReader
+public class ExifReader implements JpegSegmentMetadataReader
 {
-    // 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. */
+    /**
+     * The offset at which the TIFF data actually starts. This may be necessary when, for example, processing
+     * JPEG Exif data from APP0 which has a 6-byte preamble before starting the TIFF data.
+     */
+    private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
+
+    private boolean _storeThumbnailBytes = true;
+
+    public boolean isStoreThumbnailBytes()
+    {
+        return _storeThumbnailBytes;
+    }
+
+    public void setStoreThumbnailBytes(boolean storeThumbnailBytes)
+    {
+        _storeThumbnailBytes = storeThumbnailBytes;
+    }
+
     @NotNull
-    private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 };
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Arrays.asList(JpegSegmentType.APP1);
+    }
 
-    /** The number of formats known. */
-    private static final int MAX_FORMAT_CODE = 12;
+    public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType)
+    {
+        return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE);
+    }
 
-    // Format types
-    // 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 void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
+    {
+        if (segmentBytes == null)
+            throw new NullPointerException("segmentBytes cannot be null");
+        if (metadata == null)
+            throw new NullPointerException("metadata cannot be null");
+        if (segmentType == null)
+            throw new NullPointerException("segmentType cannot be null");
 
-    /** 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;
-    /** This tag is a pointer to the Exif Makernote IFD. */
-    public static final int TAG_MAKER_NOTE_OFFSET = 0x927C;
+        try {
+            ByteArrayReader reader = new ByteArrayReader(segmentBytes);
 
-    public static final int TIFF_HEADER_START_OFFSET = 6;
-
-    /**
-     * 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 void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
-    {
-        final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
-
-        // check for the header length
-        if (reader.getLength() <= 14) {
-            directory.addError("Exif data segment must contain at least 14 bytes");
-            return;
-        }
-
-        // check for the header preamble
-        try {
-            if (!reader.getString(0, 6).equals("Exif\0\0")) {
-                directory.addError("Exif data segment doesn't begin with 'Exif'");
+            //
+            // Check for the header preamble
+            //
+            try {
+                if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) {
+                    // TODO what do to with this error state?
+                    System.err.println("Invalid JPEG Exif segment preamble");
+                    return;
+                }
+            } catch (IOException e) {
+                // TODO what do to with this error state?
+                e.printStackTrace(System.err);
                 return;
             }
 
-            extractIFD(metadata, metadata.getOrCreateDirectory(ExifIFD0Directory.class), TIFF_HEADER_START_OFFSET, reader);
-        } catch (BufferBoundsException e) {
-            directory.addError("Exif data segment ended prematurely");
+            //
+            // Read the TIFF-formatted Exif data
+            //
+            new TiffReader().processTiff(
+                reader,
+                new ExifTiffHandler(metadata, _storeThumbnailBytes),
+                JPEG_EXIF_SEGMENT_PREAMBLE.length()
+            );
+
+        } catch (TiffProcessingException e) {
+            // TODO what do to with this error state?
+            e.printStackTrace(System.err);
+        } catch (IOException e) {
+            // TODO what do to with this error state?
+            e.printStackTrace(System.err);
         }
     }
-
-    /**
-     * 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 = 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;
-        }
-
-        // Check the next two values for correctness.
-        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
-            firstDirectoryOffset = 14;
-        }
-
-        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
-        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
-     * <p/>
-     * Then for each tag
-     * 2 bytes: tag type
-     * 2 bytes: format code
-     * 4 bytes: component count
-     */
-    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.contains(Integer.valueOf(dirStartOffset)))
-            return;
-
-        // remember that we've visited this directory so that we don't visit it again later
-        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;
-        }
-
-        // Handle each tag in this directory
-        for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {
-            final int tagOffset = calculateTagOffset(dirStartOffset, tagNumber);
-
-            // 2 bytes for the tag type
-            final int tagType = reader.getUInt16(tagOffset);
-
-            // 2 bytes for the format code
-            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);
-                continue; // JOSM patch to fix #9030
-            }
-
-            // 4 bytes dictate the number of components in this tag's data
-            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;
-            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;
-            }
-
-            // 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 > reader.getLength()) {
-                directory.addError("Illegal number of bytes: " + byteCount);
-                continue;
-            }
-
-            switch (tagType) {
-                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: {
-                    final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
-                    processDirectory(metadata.getOrCreateDirectory(ExifInteropDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
-                    continue;
-                }
-                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_OFFSET: {
-                    processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset, metadata, reader);
-                    continue;
-                }
-                default: {
-                    processTag(directory, tagType, tagValueOffset, componentCount, formatCode, reader);
-                    break;
-                }
-            }
-        }
-
-        // at the end of each IFD is an optional link to the next IFD
-        final int finalTagOffset = calculateTagOffset(dirStartOffset, dirTagCount);
-        int nextDirectoryOffset = reader.getInt32(finalTagOffset);
-        if (nextDirectoryOffset != 0) {
-            nextDirectoryOffset += tiffHeaderOffset;
-            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
-                return;
-            } else if (nextDirectoryOffset < dirStartOffset) {
-                // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory
-                return;
-            }
-            // 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 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 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:                  **
-                 * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
-                 * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
-                 * Type 3:                  **
-                 * :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*...
-                 * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200
-                 */
-                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
-                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
-            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 + 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.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.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.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
-            // Offsets are relative to the start of the current IFD tag, not the TIFF header
-            // Observed for:
-            // - Pentax ist D
-            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
-            // Offsets are relative to the start of the current IFD tag, not the TIFF header
-            // Observed for:
-            // - PENTAX Optio 330
-            // - PENTAX Optio 430
-            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...
-        }
-    }
-
-    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) {
-            case FMT_UNDEFINED:
-                // this includes exif user comments
-                directory.setByteArray(tagType, reader.getBytes(tagValueOffset, componentCount));
-                break;
-            case FMT_STRING:
-                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) {
-                    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(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) {
-                    directory.setInt(tagType, reader.getUInt8(tagValueOffset));
-                } else {
-                    int[] bytes = new int[componentCount];
-                    for (int i = 0; i < componentCount; i++)
-                        bytes[i] = reader.getUInt8(tagValueOffset + i);
-                    directory.setIntArray(tagType, bytes);
-                }
-                break;
-            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] = reader.getUInt16(tagValueOffset + (i * 2));
-                    directory.setIntArray(tagType, ints);
-                }
-                break;
-            case FMT_SSHORT:
-                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] = reader.getInt16(tagValueOffset + (i * 2));
-                    directory.setIntArray(tagType, ints);
-                }
-                break;
-            case FMT_SLONG:
-            case FMT_ULONG:
-                // 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] = reader.getInt32(tagValueOffset + (i * 4));
-                    directory.setIntArray(tagType, ints);
-                }
-                break;
-            default:
-                directory.addError("Unknown format code " + formatCode + " for tag " + tagType);
-        }
-    }
-
-    /**
-     * 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
-     */
-    private int calculateTagOffset(int dirStartOffset, int entryNumber)
-    {
-        // add 2 bytes for the tag count
-        // each entry is 12 bytes, so we skip 12 * the number seen so far
-        return dirStartOffset + 2 + (12 * entryNumber);
-    }
 }
Index: /trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
@@ -32,8 +32,10 @@
 import java.util.Map;
 
+import static com.drew.metadata.exif.ExifSubIFDDirectory.*;
+
 /**
- * Provides human-readable string representations of tag values stored in a <code>ExifSubIFDDirectory</code>.
+ * Provides human-readable string representations of tag values stored in a {@link ExifSubIFDDirectory}.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory>
@@ -60,113 +62,115 @@
 
     /**
-     * Returns a descriptive value of the the specified tag for this image.
+     * Returns a descriptive value of 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.
      */
+    @Override
     @Nullable
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE:
+            case TAG_NEW_SUBFILE_TYPE:
                 return getNewSubfileTypeDescription();
-            case ExifSubIFDDirectory.TAG_SUBFILE_TYPE:
+            case TAG_SUBFILE_TYPE:
                 return getSubfileTypeDescription();
-            case ExifSubIFDDirectory.TAG_THRESHOLDING:
+            case TAG_THRESHOLDING:
                 return getThresholdingDescription();
-            case ExifSubIFDDirectory.TAG_FILL_ORDER:
+            case TAG_FILL_ORDER:
                 return getFillOrderDescription();
-            case ExifSubIFDDirectory.TAG_EXPOSURE_TIME:
+            case TAG_EXPOSURE_TIME:
                 return getExposureTimeDescription();
-            case ExifSubIFDDirectory.TAG_SHUTTER_SPEED:
+            case TAG_SHUTTER_SPEED:
                 return getShutterSpeedDescription();
-            case ExifSubIFDDirectory.TAG_FNUMBER:
+            case TAG_FNUMBER:
                 return getFNumberDescription();
-            case ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
+            case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
                 return getCompressedAverageBitsPerPixelDescription();
-            case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE:
+            case TAG_SUBJECT_DISTANCE:
                 return getSubjectDistanceDescription();
-            case ExifSubIFDDirectory.TAG_METERING_MODE:
+            case TAG_METERING_MODE:
                 return getMeteringModeDescription();
-            case ExifSubIFDDirectory.TAG_WHITE_BALANCE:
+            case TAG_WHITE_BALANCE:
                 return getWhiteBalanceDescription();
-            case ExifSubIFDDirectory.TAG_FLASH:
+            case TAG_FLASH:
                 return getFlashDescription();
-            case ExifSubIFDDirectory.TAG_FOCAL_LENGTH:
+            case TAG_FOCAL_LENGTH:
                 return getFocalLengthDescription();
-            case ExifSubIFDDirectory.TAG_COLOR_SPACE:
+            case TAG_COLOR_SPACE:
                 return getColorSpaceDescription();
-            case ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH:
+            case TAG_EXIF_IMAGE_WIDTH:
                 return getExifImageWidthDescription();
-            case ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT:
+            case TAG_EXIF_IMAGE_HEIGHT:
                 return getExifImageHeightDescription();
-            case ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT:
+            case TAG_FOCAL_PLANE_RESOLUTION_UNIT:
                 return getFocalPlaneResolutionUnitDescription();
-            case ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES:
+            case TAG_FOCAL_PLANE_X_RESOLUTION:
                 return getFocalPlaneXResolutionDescription();
-            case ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES:
+            case TAG_FOCAL_PLANE_Y_RESOLUTION:
                 return getFocalPlaneYResolutionDescription();
-            case ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE:
+            case TAG_BITS_PER_SAMPLE:
                 return getBitsPerSampleDescription();
-            case ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
+            case TAG_PHOTOMETRIC_INTERPRETATION:
                 return getPhotometricInterpretationDescription();
-            case ExifSubIFDDirectory.TAG_ROWS_PER_STRIP:
+            case TAG_ROWS_PER_STRIP:
                 return getRowsPerStripDescription();
-            case ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS:
+            case TAG_STRIP_BYTE_COUNTS:
                 return getStripByteCountsDescription();
-            case ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL:
+            case TAG_SAMPLES_PER_PIXEL:
                 return getSamplesPerPixelDescription();
-            case ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION:
+            case TAG_PLANAR_CONFIGURATION:
                 return getPlanarConfigurationDescription();
-            case ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING:
+            case TAG_YCBCR_SUBSAMPLING:
                 return getYCbCrSubsamplingDescription();
-            case ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM:
+            case TAG_EXPOSURE_PROGRAM:
                 return getExposureProgramDescription();
-            case ExifSubIFDDirectory.TAG_APERTURE:
+            case TAG_APERTURE:
                 return getApertureValueDescription();
-            case ExifSubIFDDirectory.TAG_MAX_APERTURE:
+            case TAG_MAX_APERTURE:
                 return getMaxApertureValueDescription();
-            case ExifSubIFDDirectory.TAG_SENSING_METHOD:
+            case TAG_SENSING_METHOD:
                 return getSensingMethodDescription();
-            case ExifSubIFDDirectory.TAG_EXPOSURE_BIAS:
+            case TAG_EXPOSURE_BIAS:
                 return getExposureBiasDescription();
-            case ExifSubIFDDirectory.TAG_FILE_SOURCE:
+            case TAG_FILE_SOURCE:
                 return getFileSourceDescription();
-            case ExifSubIFDDirectory.TAG_SCENE_TYPE:
+            case TAG_SCENE_TYPE:
                 return getSceneTypeDescription();
-            case ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION:
+            case TAG_COMPONENTS_CONFIGURATION:
                 return getComponentConfigurationDescription();
-            case ExifSubIFDDirectory.TAG_EXIF_VERSION:
+            case TAG_EXIF_VERSION:
                 return getExifVersionDescription();
-            case ExifSubIFDDirectory.TAG_FLASHPIX_VERSION:
+            case TAG_FLASHPIX_VERSION:
                 return getFlashPixVersionDescription();
-            case ExifSubIFDDirectory.TAG_ISO_EQUIVALENT:
+            case TAG_ISO_EQUIVALENT:
                 return getIsoEquivalentDescription();
-            case ExifSubIFDDirectory.TAG_USER_COMMENT:
+            case TAG_USER_COMMENT:
                 return getUserCommentDescription();
-            case ExifSubIFDDirectory.TAG_CUSTOM_RENDERED:
+            case TAG_CUSTOM_RENDERED:
                 return getCustomRenderedDescription();
-            case ExifSubIFDDirectory.TAG_EXPOSURE_MODE:
+            case TAG_EXPOSURE_MODE:
                 return getExposureModeDescription();
-            case ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE:
+            case TAG_WHITE_BALANCE_MODE:
                 return getWhiteBalanceModeDescription();
-            case ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO:
+            case TAG_DIGITAL_ZOOM_RATIO:
                 return getDigitalZoomRatioDescription();
-            case ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
+            case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
                 return get35mmFilmEquivFocalLengthDescription();
-            case ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE:
+            case TAG_SCENE_CAPTURE_TYPE:
                 return getSceneCaptureTypeDescription();
-            case ExifSubIFDDirectory.TAG_GAIN_CONTROL:
+            case TAG_GAIN_CONTROL:
                 return getGainControlDescription();
-            case ExifSubIFDDirectory.TAG_CONTRAST:
+            case TAG_CONTRAST:
                 return getContrastDescription();
-            case ExifSubIFDDirectory.TAG_SATURATION:
+            case TAG_SATURATION:
                 return getSaturationDescription();
-            case ExifSubIFDDirectory.TAG_SHARPNESS:
+            case TAG_SHARPNESS:
                 return getSharpnessDescription();
-            case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE:
+            case TAG_SUBJECT_DISTANCE_RANGE:
                 return getSubjectDistanceRangeDescription();
             default:
@@ -178,18 +182,13 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page reduced-resolution image",
+            "Transparency mask",
+            "Transparency mask of reduced-resolution image",
+            "Transparency mask of multi-page image",
+            "Transparency mask of reduced-resolution multi-page image"
+        );
     }
 
@@ -197,14 +196,9 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_SUBFILE_TYPE, 1,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page image"
+        );
     }
 
@@ -212,14 +206,9 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_THRESHOLDING, 1,
+            "No dithering or halftoning",
+            "Ordered dither or halftone",
+            "Randomized dither"
+        );
     }
 
@@ -227,13 +216,8 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_FILL_ORDER, 1,
+            "Normal",
+            "Reversed"
+        );
     }
 
@@ -241,15 +225,10 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE,
+            "Unknown",
+            "Macro",
+            "Close view",
+            "Distant view"
+        );
     }
 
@@ -257,14 +236,9 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_SHARPNESS,
+            "None",
+            "Low",
+            "Hard"
+        );
     }
 
@@ -272,14 +246,9 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_SATURATION,
+            "None",
+            "Low saturation",
+            "High saturation"
+        );
     }
 
@@ -287,14 +256,9 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_CONTRAST,
+            "None",
+            "Soft",
+            "Hard"
+        );
     }
 
@@ -302,16 +266,11 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_GAIN_CONTROL,
+            "None",
+            "Low gain up",
+            "Low gain down",
+            "High gain up",
+            "High gain down"
+        );
     }
 
@@ -319,15 +278,10 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE,
+            "Standard",
+            "Landscape",
+            "Portrait",
+            "Night scene"
+        );
     }
 
@@ -335,11 +289,10 @@
     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";
+        Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
+        return value == null
+            ? null
+            : value == 0
+            ? "Unknown"
+            : SimpleDecimalFormatter.format(value) + "mm";
     }
 
@@ -347,10 +300,10 @@
     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());
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+            ? "Digital zoom not used."
+            : SimpleDecimalFormatter.format(value.doubleValue());
     }
 
@@ -358,13 +311,8 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_WHITE_BALANCE_MODE,
+            "Auto white balance",
+            "Manual white balance"
+        );
     }
 
@@ -372,14 +320,9 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_EXPOSURE_MODE,
+            "Auto exposure",
+            "Manual exposure",
+            "Auto bracket"
+        );
     }
 
@@ -387,13 +330,8 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_CUSTOM_RENDERED,
+            "Normal process",
+            "Custom process"
+        );
     }
 
@@ -401,6 +339,6 @@
     public String getUserCommentDescription()
     {
-        byte[] commentBytes = _directory.getByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT);
-        if (commentBytes==null)
+        byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT);
+        if (commentBytes == null)
             return null;
         if (commentBytes.length == 0)
@@ -408,7 +346,7 @@
 
         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".
+        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 {
@@ -442,10 +380,10 @@
     {
         // 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;
+        Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT);
         // 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);
+        return isoEquiv != null
+            ? Integer.toString(isoEquiv)
+            : null;
     }
 
@@ -453,8 +391,5 @@
     public String getExifVersionDescription()
     {
-        int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_EXIF_VERSION);
-        if (ints==null)
-            return null;
-        return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2);
+        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
     }
 
@@ -462,8 +397,5 @@
     public String getFlashPixVersionDescription()
     {
-        int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_FLASHPIX_VERSION);
-        if (ints==null)
-            return null;
-        return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2);
+        return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2);
     }
 
@@ -471,10 +403,8 @@
     public String getSceneTypeDescription()
     {
-        Integer sceneType = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_TYPE);
-        if (sceneType==null)
-            return null;
-        return sceneType == 1
-                ? "Directly photographed image"
-                : "Unknown (" + sceneType + ")";
+        return getIndexedDescription(TAG_SCENE_TYPE,
+            1,
+            "Directly photographed image"
+        );
     }
 
@@ -482,10 +412,10 @@
     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 + ")";
+        return getIndexedDescription(TAG_FILE_SOURCE,
+            1,
+            "Film Scanner",
+            "Reflection Print Scanner",
+            "Digital Still Camera (DSC)"
+        );
     }
 
@@ -493,6 +423,6 @@
     public String getExposureBiasDescription()
     {
-        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_EXPOSURE_BIAS);
-        if (value==null)
+        Rational value = _directory.getRational(TAG_EXPOSURE_BIAS);
+        if (value == null)
             return null;
         return value.toSimpleString(true) + " EV";
@@ -502,6 +432,6 @@
     public String getMaxApertureValueDescription()
     {
-        Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_MAX_APERTURE);
-        if (aperture==null)
+        Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE);
+        if (aperture == null)
             return null;
         double fStop = PhotographicConversions.apertureToFStop(aperture);
@@ -512,6 +442,6 @@
     public String getApertureValueDescription()
     {
-        Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_APERTURE);
-        if (aperture==null)
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
+        if (aperture == null)
             return null;
         double fStop = PhotographicConversions.apertureToFStop(aperture);
@@ -522,22 +452,15 @@
     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 + ")";
-        }
+        return getIndexedDescription(TAG_EXPOSURE_PROGRAM,
+            1,
+            "Manual control",
+            "Program normal",
+            "Aperture priority",
+            "Shutter priority",
+            "Program creative (slow program)",
+            "Program action (high-speed program)",
+            "Portrait mode",
+            "Landscape mode"
+        );
     }
 
@@ -545,6 +468,6 @@
     public String getYCbCrSubsamplingDescription()
     {
-        int[] positions = _directory.getIntArray(ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING);
-        if (positions==null)
+        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
+        if (positions == null)
             return null;
         if (positions[0] == 2 && positions[1] == 1) {
@@ -564,13 +487,9 @@
         // 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";
-        }
+        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
+            1,
+            "Chunky (contiguous for each subsampling pixel)",
+            "Separate (Y-plane/Cb-plane/Cr-plane format)"
+        );
     }
 
@@ -578,6 +497,6 @@
     public String getSamplesPerPixelDescription()
     {
-        String value = _directory.getString(ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL);
-        return value==null ? null : value + " samples/pixel";
+        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
+        return value == null ? null : value + " samples/pixel";
     }
 
@@ -585,6 +504,6 @@
     public String getRowsPerStripDescription()
     {
-        final String value = _directory.getString(ExifSubIFDDirectory.TAG_ROWS_PER_STRIP);
-        return value==null ? null : value + " rows/strip";
+        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
+        return value == null ? null : value + " rows/strip";
     }
 
@@ -592,6 +511,6 @@
     public String getStripByteCountsDescription()
     {
-        final String value = _directory.getString(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
-        return value==null ? null : value + " bytes";
+        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
+        return value == null ? null : value + " bytes";
     }
 
@@ -600,6 +519,6 @@
     {
         // Shows the color space of the image data components
-        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
-        if (value==null)
+        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value == null)
             return null;
         switch (value) {
@@ -626,6 +545,6 @@
     public String getBitsPerSampleDescription()
     {
-        String value = _directory.getString(ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE);
-        return value==null ? null : value + " bits/component/pixel";
+        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
+        return value == null ? null : value + " bits/component/pixel";
     }
 
@@ -633,10 +552,10 @@
     public String getFocalPlaneXResolutionDescription()
     {
-        Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES);
-        if (rational==null)
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION);
+        if (rational == null)
             return null;
         final String unit = getFocalPlaneResolutionUnitDescription();
         return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
-            + (unit==null ? "" : " " + unit.toLowerCase());
+            + (unit == null ? "" : " " + unit.toLowerCase());
     }
 
@@ -644,10 +563,10 @@
     public String getFocalPlaneYResolutionDescription()
     {
-        Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES);
-        if (rational==null)
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION);
+        if (rational == null)
             return null;
         final String unit = getFocalPlaneResolutionUnitDescription();
         return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
-            + (unit==null ? "" : " " + unit.toLowerCase());
+            + (unit == null ? "" : " " + unit.toLowerCase());
     }
 
@@ -655,16 +574,12 @@
     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 "";
-        }
+        // Unit of FocalPlaneXResolution/FocalPlaneYResolution.
+        // '1' means no-unit, '2' inch, '3' centimeter.
+        return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+            1,
+            "(No unit)",
+            "Inches",
+            "cm"
+        );
     }
 
@@ -672,8 +587,6 @@
     public String getExifImageWidthDescription()
     {
-        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH);
-        if (value==null)
-            return null;
-        return value + " pixels";
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH);
+        return value == null ? null : value + " pixels";
     }
 
@@ -681,8 +594,6 @@
     public String getExifImageHeightDescription()
     {
-        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT);
-        if (value==null)
-            return null;
-        return value + " pixels";
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT);
+        return value == null ? null : value + " pixels";
     }
 
@@ -690,6 +601,6 @@
     public String getColorSpaceDescription()
     {
-        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_COLOR_SPACE);
-        if (value==null)
+        final Integer value = _directory.getInteger(TAG_COLOR_SPACE);
+        if (value == null)
             return null;
         if (value == 1)
@@ -697,5 +608,5 @@
         if (value == 65535)
             return "Undefined";
-        return "Unknown";
+        return "Unknown (" + value + ")";
     }
 
@@ -703,6 +614,6 @@
     public String getFocalLengthDescription()
     {
-        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_LENGTH);
-        if (value==null)
+        Rational value = _directory.getRational(TAG_FOCAL_LENGTH);
+        if (value == null)
             return null;
         java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
@@ -714,5 +625,5 @@
     {
         /*
-         * This is a bitmask.
+         * This is a bit mask.
          * 0 = flash fired
          * 1 = return detected
@@ -724,12 +635,12 @@
          */
 
-        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FLASH);
-
-        if (value==null)
+        final Integer value = _directory.getInteger(TAG_FLASH);
+
+        if (value == null)
             return null;
 
         StringBuilder sb = new StringBuilder();
 
-        if ((value & 0x1)!=0)
+        if ((value & 0x1) != 0)
             sb.append("Flash fired");
         else
@@ -737,7 +648,6 @@
 
         // check if we're able to detect a return, before we mention it
-        if ((value & 0x4)!=0)
-        {
-            if ((value & 0x2)!=0)
+        if ((value & 0x4) != 0) {
+            if ((value & 0x2) != 0)
                 sb.append(", return detected");
             else
@@ -745,8 +655,8 @@
         }
 
-        if ((value & 0x10)!=0)
+        if ((value & 0x10) != 0)
             sb.append(", auto");
 
-        if ((value & 0x40)!=0)
+        if ((value & 0x40) != 0)
             sb.append(", red-eye reduction");
 
@@ -760,6 +670,6 @@
         // '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)
+        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
             return null;
         switch (value) {
@@ -786,6 +696,6 @@
         // '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)
+        Integer value = _directory.getInteger(TAG_METERING_MODE);
+        if (value == null)
             return null;
         switch (value) {
@@ -806,6 +716,6 @@
     public String getSubjectDistanceDescription()
     {
-        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE);
-        if (value==null)
+        Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE);
+        if (value == null)
             return null;
         java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
@@ -816,13 +726,11 @@
     public String getCompressedAverageBitsPerPixelDescription()
     {
-        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);
-        if (value==null)
+        Rational value = _directory.getRational(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";
-        }
+        return value.isInteger() && value.intValue() == 1
+            ? ratio + " bit/pixel"
+            : ratio + " bits/pixel";
     }
 
@@ -830,6 +738,6 @@
     public String getExposureTimeDescription()
     {
-        String value = _directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME);
-        return value==null ? null : value + " sec";
+        String value = _directory.getString(TAG_EXPOSURE_TIME);
+        return value == null ? null : value + " sec";
     }
 
@@ -847,14 +755,14 @@
         // 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))));
+        Float apexValue = _directory.getFloatObject(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;
+            float fApexPower = (float)apexPower10 / 10.0f;
             return fApexPower + " sec";
         } else {
-            int apexPower = (int)((Math.exp(apexValue*Math.log(2))));
+            int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
             return "1/" + apexPower + " sec";
         }
@@ -879,5 +787,4 @@
         return sb.toString();
 */
-
     }
 
@@ -885,6 +792,6 @@
     public String getFNumberDescription()
     {
-        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FNUMBER);
-        if (value==null)
+        Rational value = _directory.getRational(TAG_FNUMBER);
+        if (value == null)
             return null;
         return "F" + SimpleDecimalFormatter.format(value.doubleValue());
@@ -897,18 +804,15 @@
         // '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 "";
-        }
+        return getIndexedDescription(TAG_SENSING_METHOD,
+            1,
+            "(Not defined)",
+            "One-chip color area sensor",
+            "Two-chip color area sensor",
+            "Three-chip color area sensor",
+            "Color sequential area sensor",
+            null,
+            "Trilinear sensor",
+            "Color sequential linear sensor"
+        );
     }
 
@@ -916,6 +820,6 @@
     public String getComponentConfigurationDescription()
     {
-        int[] components = _directory.getIntArray(ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION);
-        if (components==null)
+        int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION);
+        if (components == null)
             return null;
         String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
Index: /trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
@@ -29,5 +29,5 @@
  * Describes Exif tags from the SubIFD directory.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifSubIFDDirectory extends Directory
@@ -133,7 +133,7 @@
     /**
      * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.
-     * <p/>
+     * <p>
      * OECF is the relationship between the camera optical input and the image values.
-     * <p/>
+     * <p>
      * The values are:
      * <ul>
@@ -260,8 +260,20 @@
      */
     public static final int TAG_FOCAL_LENGTH = 0x920A;
+
+    /**
+     * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs.
+     * To determine the format, we consider the starting bytes of the makernote itself and sometimes the
+     * camera model and make.
+     * <p>
+     * The component count for this tag includes all of the bytes needed for the makernote.
+     */
+    public static final int TAG_MAKERNOTE = 0x927C;
+
     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;
     /**
@@ -274,6 +286,10 @@
     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;
+
+    /** This tag is a pointer to the Exif Interop IFD. */
+    public static final int TAG_INTEROP_OFFSET = 0xA005;
+
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xA20E;
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xA20F;
     /**
      * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
@@ -285,5 +301,5 @@
      * 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_FOCAL_PLANE_RESOLUTION_UNIT = 0xA210;
     public static final int TAG_EXPOSURE_INDEX = 0xA215;
     public static final int TAG_SENSING_METHOD = 0xA217;
@@ -512,6 +528,6 @@
         _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_MAKERNOTE, "Makernote");
+        _tagNameMap.put(TAG_INTEROP_OFFSET, "Interoperability Offset");
 
         _tagNameMap.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type");
@@ -586,9 +602,9 @@
         _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");
+        _tagNameMap.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution");
         // 0x920F in TIFF/EP
-        _tagNameMap.put(TAG_FOCAL_PLANE_Y_RES, "Focal Plane Y Resolution");
+        _tagNameMap.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution");
         // 0x9210 in TIFF/EP
-        _tagNameMap.put(TAG_FOCAL_PLANE_UNIT, "Focal Plane Resolution Unit");
+        _tagNameMap.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit");
         // 0x9214 in TIFF/EP
         _tagNameMap.put(TAG_SUBJECT_LOCATION_2, "Subject Location");
@@ -614,5 +630,5 @@
         _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");
@@ -634,4 +650,5 @@
     }
 
+    @Override
     @NotNull
     public String getName()
@@ -640,4 +657,5 @@
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
Index: /trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -27,8 +27,10 @@
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
+
 /**
- * Provides human-readable string representations of tag values stored in a <code>ExifThumbnailDirectory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
+ * Provides human-readable string representations of tag values stored in a {@link ExifThumbnailDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirectory>
@@ -52,51 +54,53 @@
 
     /**
-     * Returns a descriptive value of the the specified tag for this image.
+     * Returns a descriptive value of 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.
      */
+    @Override
     @Nullable
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case ExifThumbnailDirectory.TAG_ORIENTATION:
+            case TAG_ORIENTATION:
                 return getOrientationDescription();
-            case ExifThumbnailDirectory.TAG_RESOLUTION_UNIT:
+            case TAG_RESOLUTION_UNIT:
                 return getResolutionDescription();
-            case ExifThumbnailDirectory.TAG_YCBCR_POSITIONING:
+            case TAG_YCBCR_POSITIONING:
                 return getYCbCrPositioningDescription();
-            case ExifThumbnailDirectory.TAG_X_RESOLUTION:
+            case TAG_X_RESOLUTION:
                 return getXResolutionDescription();
-            case ExifThumbnailDirectory.TAG_Y_RESOLUTION:
+            case TAG_Y_RESOLUTION:
                 return getYResolutionDescription();
-            case ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET:
+            case TAG_THUMBNAIL_OFFSET:
                 return getThumbnailOffsetDescription();
-            case ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH:
+            case TAG_THUMBNAIL_LENGTH:
                 return getThumbnailLengthDescription();
-            case ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH:
+            case TAG_THUMBNAIL_IMAGE_WIDTH:
                 return getThumbnailImageWidthDescription();
-            case ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT:
+            case TAG_THUMBNAIL_IMAGE_HEIGHT:
                 return getThumbnailImageHeightDescription();
-            case ExifThumbnailDirectory.TAG_BITS_PER_SAMPLE:
+            case TAG_BITS_PER_SAMPLE:
                 return getBitsPerSampleDescription();
-            case ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION:
+            case TAG_THUMBNAIL_COMPRESSION:
                 return getCompressionDescription();
-            case ExifThumbnailDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
+            case TAG_PHOTOMETRIC_INTERPRETATION:
                 return getPhotometricInterpretationDescription();
-            case ExifThumbnailDirectory.TAG_ROWS_PER_STRIP:
+            case TAG_ROWS_PER_STRIP:
                 return getRowsPerStripDescription();
-            case ExifThumbnailDirectory.TAG_STRIP_BYTE_COUNTS:
+            case TAG_STRIP_BYTE_COUNTS:
                 return getStripByteCountsDescription();
-            case ExifThumbnailDirectory.TAG_SAMPLES_PER_PIXEL:
+            case TAG_SAMPLES_PER_PIXEL:
                 return getSamplesPerPixelDescription();
-            case ExifThumbnailDirectory.TAG_PLANAR_CONFIGURATION:
+            case TAG_PLANAR_CONFIGURATION:
                 return getPlanarConfigurationDescription();
-            case ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING:
+            case TAG_YCBCR_SUBSAMPLING:
                 return getYCbCrSubsamplingDescription();
-            case ExifThumbnailDirectory.TAG_REFERENCE_BLACK_WHITE:
+            case TAG_REFERENCE_BLACK_WHITE:
                 return getReferenceBlackWhiteDescription();
             default:
@@ -108,6 +112,6 @@
     public String getReferenceBlackWhiteDescription()
     {
-        int[] ints = _directory.getIntArray(ExifThumbnailDirectory.TAG_REFERENCE_BLACK_WHITE);
-        if (ints==null)
+        int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
+        if (ints == null || ints.length < 6)
             return null;
         int blackR = ints[0];
@@ -117,6 +121,5 @@
         int blackB = ints[4];
         int whiteB = ints[5];
-        return "[" + blackR + "," + blackG + "," + blackB + "] " +
-               "[" + whiteR + "," + whiteG + "," + whiteB + "]";
+        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
     }
 
@@ -124,6 +127,6 @@
     public String getYCbCrSubsamplingDescription()
     {
-        int[] positions = _directory.getIntArray(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING);
-        if (positions==null || positions.length < 2)
+        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
+        if (positions == null || positions.length < 2)
             return null;
         if (positions[0] == 2 && positions[1] == 1) {
@@ -143,13 +146,9 @@
         // 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";
-        }
+        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
+            1,
+            "Chunky (contiguous for each subsampling pixel)",
+            "Separate (Y-plane/Cb-plane/Cr-plane format)"
+        );
     }
 
@@ -157,6 +156,6 @@
     public String getSamplesPerPixelDescription()
     {
-        String value = _directory.getString(ExifThumbnailDirectory.TAG_SAMPLES_PER_PIXEL);
-        return value==null ? null : value + " samples/pixel";
+        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
+        return value == null ? null : value + " samples/pixel";
     }
 
@@ -164,6 +163,6 @@
     public String getRowsPerStripDescription()
     {
-        final String value = _directory.getString(ExifThumbnailDirectory.TAG_ROWS_PER_STRIP);
-        return value==null ? null : value + " rows/strip";
+        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
+        return value == null ? null : value + " rows/strip";
     }
 
@@ -171,6 +170,6 @@
     public String getStripByteCountsDescription()
     {
-        final String value = _directory.getString(ExifThumbnailDirectory.TAG_STRIP_BYTE_COUNTS);
-        return value==null ? null : value + " bytes";
+        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
+        return value == null ? null : value + " bytes";
     }
 
@@ -179,6 +178,6 @@
     {
         // Shows the color space of the image data components
-        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
-        if (value==null)
+        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value == null)
             return null;
         switch (value) {
@@ -205,6 +204,6 @@
     public String getCompressionDescription()
     {
-        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION);
-        if (value==null)
+        Integer value = _directory.getInteger(TAG_THUMBNAIL_COMPRESSION);
+        if (value == null)
             return null;
         switch (value) {
@@ -244,6 +243,6 @@
     public String getBitsPerSampleDescription()
     {
-        String value = _directory.getString(ExifThumbnailDirectory.TAG_BITS_PER_SAMPLE);
-        return value==null ? null : value + " bits/component/pixel";
+        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
+        return value == null ? null : value + " bits/component/pixel";
     }
 
@@ -251,6 +250,6 @@
     public String getThumbnailImageWidthDescription()
     {
-        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
-        return value==null ? null : value + " pixels";
+        String value = _directory.getString(TAG_THUMBNAIL_IMAGE_WIDTH);
+        return value == null ? null : value + " pixels";
     }
 
@@ -258,6 +257,6 @@
     public String getThumbnailImageHeightDescription()
     {
-        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
-        return value==null ? null : value + " pixels";
+        String value = _directory.getString(TAG_THUMBNAIL_IMAGE_HEIGHT);
+        return value == null ? null : value + " pixels";
     }
 
@@ -265,6 +264,6 @@
     public String getThumbnailLengthDescription()
     {
-        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
-        return value==null ? null : value + " bytes";
+        String value = _directory.getString(TAG_THUMBNAIL_LENGTH);
+        return value == null ? null : value + " bytes";
     }
 
@@ -272,6 +271,6 @@
     public String getThumbnailOffsetDescription()
     {
-        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
-        return value==null ? null : value + " bytes";
+        String value = _directory.getString(TAG_THUMBNAIL_OFFSET);
+        return value == null ? null : value + " bytes";
     }
 
@@ -279,11 +278,11 @@
     public String getYResolutionDescription()
     {
-        Rational value = _directory.getRational(ExifThumbnailDirectory.TAG_Y_RESOLUTION);
-        if (value==null)
+        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
+        if (value == null)
             return null;
         final String unit = getResolutionDescription();
         return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
-                " dots per " +
-                (unit==null ? "unit" : unit.toLowerCase());
+            " dots per " +
+            (unit == null ? "unit" : unit.toLowerCase());
     }
 
@@ -291,11 +290,11 @@
     public String getXResolutionDescription()
     {
-        Rational value = _directory.getRational(ExifThumbnailDirectory.TAG_X_RESOLUTION);
-        if (value==null)
+        Rational value = _directory.getRational(TAG_X_RESOLUTION);
+        if (value == null)
             return null;
         final String unit = getResolutionDescription();
         return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
-                " dots per " +
-                (unit==null ? "unit" : unit.toLowerCase());
+            " dots per " +
+            (unit == null ? "unit" : unit.toLowerCase());
     }
 
@@ -303,13 +302,5 @@
     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);
-        }
+        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
     }
 
@@ -317,19 +308,13 @@
     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);
-        }
+        return getIndexedDescription(TAG_ORIENTATION, 1,
+            "Top, left side (Horizontal / normal)",
+            "Top, right side (Mirror horizontal)",
+            "Bottom, right side (Rotate 180)",
+            "Bottom, left side (Mirror vertical)",
+            "Left side, top (Mirror horizontal and rotate 270 CW)",
+            "Right side, top (Rotate 90 CW)",
+            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
+            "Left side, bottom (Rotate 270 CW)");
     }
 
@@ -338,14 +323,5 @@
     {
         // '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 "";
-        }
+        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
     }
 }
Index: /trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 
@@ -34,5 +34,5 @@
  * One of several Exif directories.  Otherwise known as IFD1, this directory holds information about an embedded thumbnail image.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class ExifThumbnailDirectory extends Directory
@@ -57,5 +57,5 @@
      * 7 = JPEG
      * 8 = Adobe Deflate
-     * 9 = JBIG B&W
+     * 9 = JBIG B&amp;W
      * 10 = JBIG Color
      * 32766 = Next
@@ -98,12 +98,20 @@
     public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
 
-    /** The position in the file of raster data. */
+    /**
+     * 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. */
+    /**
+     * 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. */
+    /**
+     * 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. */
+    /**
+     * The size of the raster data in bytes.
+     */
     public static final int TAG_STRIP_BYTE_COUNTS = 0x0117;
     /**
@@ -117,7 +125,11 @@
     public static final int TAG_PLANAR_CONFIGURATION = 0x011C;
     public static final int TAG_RESOLUTION_UNIT = 0x0128;
-    /** The offset to thumbnail image bytes. */
+    /**
+     * The offset to thumbnail image bytes.
+     */
     public static final int TAG_THUMBNAIL_OFFSET = 0x0201;
-    /** The size of the thumbnail image data in bytes. */
+    /**
+     * 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;
@@ -129,6 +141,5 @@
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
-    static
-    {
+    static {
         _tagNameMap.put(TAG_THUMBNAIL_IMAGE_WIDTH, "Thumbnail Image Width");
         _tagNameMap.put(TAG_THUMBNAIL_IMAGE_HEIGHT, "Thumbnail Image Height");
@@ -161,4 +172,5 @@
     }
 
+    @Override
     @NotNull
     public String getName()
@@ -167,4 +179,5 @@
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
@@ -193,5 +206,5 @@
         byte[] data = _thumbnailData;
 
-        if (data==null)
+        if (data == null)
             throw new MetadataException("No thumbnail data exists.");
 
@@ -201,5 +214,5 @@
             stream.write(data);
         } finally {
-            if (stream!=null)
+            if (stream != null)
                 stream.close();
         }
Index: /trunk/src/com/drew/metadata/exif/ExifTiffHandler.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 8132)
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.imaging.tiff.TiffProcessingException;
+import com.drew.imaging.tiff.TiffReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.exif.makernotes.*;
+import com.drew.metadata.iptc.IptcReader;
+import com.drew.metadata.tiff.DirectoryTiffHandler;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Implementation of {@link com.drew.imaging.tiff.TiffHandler} used for handling TIFF tags according to the Exif
+ * standard.
+ * <p>
+ * Includes support for camera manufacturer makernotes.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifTiffHandler extends DirectoryTiffHandler
+{
+    private final boolean _storeThumbnailBytes;
+
+    public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes)
+    {
+        super(metadata, ExifIFD0Directory.class);
+        _storeThumbnailBytes = storeThumbnailBytes;
+    }
+
+    public void setTiffMarker(int marker) throws TiffProcessingException
+    {
+        final int standardTiffMarker = 0x002A;
+        final int olympusRawTiffMarker = 0x4F52; // for ORF files
+        final int panasonicRawTiffMarker = 0x0055; // for RW2 files
+
+        if (marker != standardTiffMarker && marker != olympusRawTiffMarker && marker != panasonicRawTiffMarker) {
+            throw new TiffProcessingException("Unexpected TIFF marker: 0x" + Integer.toHexString(marker));
+        }
+    }
+
+    public boolean isTagIfdPointer(int tagType)
+    {
+        if (tagType == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
+            pushDirectory(ExifSubIFDDirectory.class);
+            return true;
+        } else if (tagType == ExifIFD0Directory.TAG_GPS_INFO_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
+            pushDirectory(GpsDirectory.class);
+            return true;
+        } else if (tagType == ExifSubIFDDirectory.TAG_INTEROP_OFFSET && _currentDirectory instanceof ExifSubIFDDirectory) {
+            pushDirectory(ExifInteropDirectory.class);
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean hasFollowerIfd()
+    {
+        // In Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case.
+        if (_currentDirectory instanceof ExifIFD0Directory) {
+            pushDirectory(ExifThumbnailDirectory.class);
+            return true;
+        }
+
+        // The Canon EOS 7D (CR2) has three chained/following thumbnail IFDs
+        if (_currentDirectory instanceof ExifThumbnailDirectory)
+            return true;
+
+        // This should not happen, as Exif doesn't use follower IFDs apart from that above.
+        // NOTE have seen the CanonMakernoteDirectory IFD have a follower pointer, but it points to invalid data.
+        return false;
+    }
+
+    public boolean customProcessTag(final int tagOffset,
+                                    final @NotNull Set<Integer> processedIfdOffsets,
+                                    final int tiffHeaderOffset,
+                                    final @NotNull RandomAccessReader reader,
+                                    final int tagId,
+                                    final int byteCount) throws IOException
+    {
+        // Custom processing for the Makernote tag
+        if (tagId == ExifSubIFDDirectory.TAG_MAKERNOTE && _currentDirectory instanceof ExifSubIFDDirectory) {
+            return processMakernote(tagOffset, processedIfdOffsets, tiffHeaderOffset, reader);
+        }
+
+        // Custom processing for embedded IPTC data
+        if (tagId == ExifSubIFDDirectory.TAG_IPTC_NAA && _currentDirectory instanceof ExifIFD0Directory) {
+            // NOTE Adobe sets type 4 for IPTC instead of 7
+            if (reader.getInt8(tagOffset) == 0x1c) {
+                final byte[] iptcBytes = reader.getBytes(tagOffset, byteCount);
+                new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length);
+                return true;
+            }
+            return false;
+        }
+
+        return false;
+    }
+
+    public void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset)
+    {
+        if (_storeThumbnailBytes) {
+            // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
+            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 (IOException ex) {
+                        thumbnailDirectory.addError("Invalid thumbnail data specification: " + ex.getMessage());
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean processMakernote(final int makernoteOffset,
+                                     final @NotNull Set<Integer> processedIfdOffsets,
+                                     final int tiffHeaderOffset,
+                                     final @NotNull RandomAccessReader reader) throws IOException
+    {
+        // Determine the camera model and makernote format.
+        Directory ifd0Directory = _metadata.getDirectory(ExifIFD0Directory.class);
+
+        if (ifd0Directory == null)
+            return false;
+
+        String cameraMake = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
+
+        final String firstTwoChars = reader.getString(makernoteOffset, 2);
+        final String firstThreeChars = reader.getString(makernoteOffset, 3);
+        final String firstFourChars = reader.getString(makernoteOffset, 4);
+        final String firstFiveChars = reader.getString(makernoteOffset, 5);
+        final String firstSixChars = reader.getString(makernoteOffset, 6);
+        final String firstSevenChars = reader.getString(makernoteOffset, 7);
+        final String firstEightChars = reader.getString(makernoteOffset, 8);
+        final String firstTwelveChars = reader.getString(makernoteOffset, 12);
+
+        boolean byteOrderBefore = reader.isMotorolaByteOrder();
+
+        if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
+            // Olympus Makernote
+            // Epson and Agfa use Olympus makernote standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/
+            pushDirectory(OlympusMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+        } else if (cameraMake != null && cameraMake.toUpperCase().startsWith("MINOLTA")) {
+            // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote
+            // area that commences immediately.
+            pushDirectory(OlympusMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+        } else if (cameraMake != null && cameraMake.trim().toUpperCase().startsWith("NIKON")) {
+            if ("Nikon".equals(firstFiveChars)) {
+                /* There are two scenarios here:
+                 * Type 1:                  **
+                 * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
+                 * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
+                 * Type 3:                  **
+                 * :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*...
+                 * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200
+                 */
+                switch (reader.getUInt8(makernoteOffset + 6)) {
+                    case 1:
+                        pushDirectory(NikonType1MakernoteDirectory.class);
+                        TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+                        break;
+                    case 2:
+                        pushDirectory(NikonType2MakernoteDirectory.class);
+                        TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 18, makernoteOffset + 10);
+                        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.
+                pushDirectory(NikonType2MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+            }
+        } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {
+            pushDirectory(SonyType1MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset);
+        } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
+            // force MM for this directory
+            reader.setMotorolaByteOrder(true);
+            // skip 12 byte header + 2 for "MM" + 6
+            pushDirectory(SonyType6MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 20, tiffHeaderOffset);
+        } else if ("SIGMA\u0000\u0000\u0000".equals(firstEightChars) || "FOVEON\u0000\u0000".equals(firstEightChars)) {
+            pushDirectory(SigmaMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 10, tiffHeaderOffset);
+        } else if ("KDK".equals(firstThreeChars)) {
+            reader.setMotorolaByteOrder(firstSevenChars.equals("KDK INFO"));
+            processKodakMakernote(_metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), makernoteOffset, reader);
+        } else if ("Canon".equalsIgnoreCase(cameraMake)) {
+            pushDirectory(CanonMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+        } else if (cameraMake != null && cameraMake.toUpperCase().startsWith("CASIO")) {
+            if ("QVC\u0000\u0000\u0000".equals(firstSixChars)) {
+                pushDirectory(CasioType2MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 6, tiffHeaderOffset);
+            } else {
+                pushDirectory(CasioType1MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+            }
+        } else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraMake)) {
+            // Note that this also applies to certain Leica cameras, such as the Digilux-4.3
+            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 = makernoteOffset + reader.getInt32(makernoteOffset + 8);
+            pushDirectory(FujifilmMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, ifdStart, makernoteOffset);
+        } else if ("KYOCERA".equals(firstSevenChars)) {
+            // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
+            pushDirectory(KyoceraMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 22, tiffHeaderOffset);
+        } else if ("LEICA".equals(firstFiveChars)) {
+            reader.setMotorolaByteOrder(false);
+            if ("Leica Camera AG".equals(cameraMake)) {
+                pushDirectory(LeicaMakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+            } else if ("LEICA".equals(cameraMake)) {
+                // Some Leica cameras use Panasonic makernote tags
+                pushDirectory(PanasonicMakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+            } else {
+                return false;
+            }
+        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 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
+            pushDirectory(PanasonicMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset);
+        } 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
+            // Offsets are relative to the start of the current IFD tag, not the TIFF header
+            // Observed for:
+            // - Pentax ist D
+            pushDirectory(CasioType2MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 6, makernoteOffset);
+        } else if (cameraMake != null && (cameraMake.toUpperCase().startsWith("PENTAX") || cameraMake.toUpperCase().startsWith("ASAHI"))) {
+            // NON-Standard TIFF IFD Data using Pentax Tags
+            // IFD has no Next-IFD pointer at end of IFD, and
+            // Offsets are relative to the start of the current IFD tag, not the TIFF header
+            // Observed for:
+            // - PENTAX Optio 330
+            // - PENTAX Optio 430
+            pushDirectory(PentaxMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, makernoteOffset);
+//        } 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 if ("SANYO\0\1\0".equals(firstEightChars)) {
+            pushDirectory(SanyoMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
+        } else if (cameraMake != null && cameraMake.toLowerCase().startsWith("ricoh")) {
+            if (firstTwoChars.equals("Rv") || firstThreeChars.equals("Rev")) {
+                // This is a textual format, where the makernote bytes look like:
+                //   Rv0103;Rg1C;Bg18;Ll0;Ld0;Aj0000;Bn0473800;Fp2E00:������������������������������
+                //   Rv0103;Rg1C;Bg18;Ll0;Ld0;Aj0000;Bn0473800;Fp2D05:������������������������������
+                //   Rv0207;Sf6C84;Rg76;Bg60;Gg42;Ll0;Ld0;Aj0004;Bn0B02900;Fp10B8;Md6700;Ln116900086D27;Sv263:0000000000000000000000��
+                // This format is currently unsupported
+                return false;
+            } else if (firstFiveChars.equalsIgnoreCase("Ricoh")) {
+                // Always in Motorola byte order
+                reader.setMotorolaByteOrder(true);
+                pushDirectory(RicohMakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
+            }
+        } else {
+            // The makernote is not comprehended by this library.
+            // If you are reading this and believe a particular camera's image should be processed, get in touch.
+            return false;
+        }
+
+        reader.setMotorolaByteOrder(byteOrderBefore);
+        return true;
+    }
+
+    private static void processKodakMakernote(@NotNull final KodakMakernoteDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader)
+    {
+        // Kodak's makernote is not in IFD format. It has values at fixed offsets.
+        int dataOffset = tagValueOffset + 8;
+        try {
+            directory.setString(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getString(dataOffset, 8));
+            directory.setInt(KodakMakernoteDirectory.TAG_QUALITY, reader.getUInt8(dataOffset + 9));
+            directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE, reader.getUInt8(dataOffset + 10));
+            directory.setInt(KodakMakernoteDirectory.TAG_IMAGE_WIDTH, reader.getUInt16(dataOffset + 12));
+            directory.setInt(KodakMakernoteDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16(dataOffset + 14));
+            directory.setInt(KodakMakernoteDirectory.TAG_YEAR_CREATED, reader.getUInt16(dataOffset + 16));
+            directory.setByteArray(KodakMakernoteDirectory.TAG_MONTH_DAY_CREATED, reader.getBytes(dataOffset + 18, 2));
+            directory.setByteArray(KodakMakernoteDirectory.TAG_TIME_CREATED, reader.getBytes(dataOffset + 20, 4));
+            directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE_2, reader.getUInt16(dataOffset + 24));
+            directory.setInt(KodakMakernoteDirectory.TAG_SHUTTER_MODE, reader.getUInt8(dataOffset + 27));
+            directory.setInt(KodakMakernoteDirectory.TAG_METERING_MODE, reader.getUInt8(dataOffset + 28));
+            directory.setInt(KodakMakernoteDirectory.TAG_SEQUENCE_NUMBER, reader.getUInt8(dataOffset + 29));
+            directory.setInt(KodakMakernoteDirectory.TAG_F_NUMBER, reader.getUInt16(dataOffset + 30));
+            directory.setLong(KodakMakernoteDirectory.TAG_EXPOSURE_TIME, reader.getUInt32(dataOffset + 32));
+            directory.setInt(KodakMakernoteDirectory.TAG_EXPOSURE_COMPENSATION, reader.getInt16(dataOffset + 36));
+            directory.setInt(KodakMakernoteDirectory.TAG_FOCUS_MODE, reader.getUInt8(dataOffset + 56));
+            directory.setInt(KodakMakernoteDirectory.TAG_WHITE_BALANCE, reader.getUInt8(dataOffset + 64));
+            directory.setInt(KodakMakernoteDirectory.TAG_FLASH_MODE, reader.getUInt8(dataOffset + 92));
+            directory.setInt(KodakMakernoteDirectory.TAG_FLASH_FIRED, reader.getUInt8(dataOffset + 93));
+            directory.setInt(KodakMakernoteDirectory.TAG_ISO_SETTING, reader.getUInt16(dataOffset + 94));
+            directory.setInt(KodakMakernoteDirectory.TAG_ISO, reader.getUInt16(dataOffset + 96));
+            directory.setInt(KodakMakernoteDirectory.TAG_TOTAL_ZOOM, reader.getUInt16(dataOffset + 98));
+            directory.setInt(KodakMakernoteDirectory.TAG_DATE_TIME_STAMP, reader.getUInt16(dataOffset + 100));
+            directory.setInt(KodakMakernoteDirectory.TAG_COLOR_MODE, reader.getUInt16(dataOffset + 102));
+            directory.setInt(KodakMakernoteDirectory.TAG_DIGITAL_ZOOM, reader.getUInt16(dataOffset + 104));
+            directory.setInt(KodakMakernoteDirectory.TAG_SHARPNESS, reader.getInt8(dataOffset + 107));
+        } catch (IOException ex) {
+            directory.addError("Error processing Kodak makernote data: " + ex.getMessage());
+        }
+    }
+}
+
Index: unk/src/com/drew/metadata/exif/FujifilmMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/FujifilmMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,337 +1,0 @@
-/*
- * 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>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
- *   align.
- * - The other manufacturer's MakerNote counts the "offset to data" from the first byte
- *   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<FujifilmMakernoteDirectory>
-{
-    public FujifilmMakernoteDescriptor(@NotNull FujifilmMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_SHARPNESS:
-                return getSharpnessDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_WHITE_BALANCE:
-                return getWhiteBalanceDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_COLOR_SATURATION:
-                return getColorDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_TONE:
-                return getToneDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_MODE:
-                return getFlashModeDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_STRENGTH:
-                return getFlashStrengthDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_MACRO:
-                return getMacroDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_MODE:
-                return getFocusModeDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_SLOW_SYNCH:
-                return getSlowSyncDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_PICTURE_MODE:
-                return getPictureModeDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING:
-                return getContinuousTakingOrAutoBrackettingDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_BLUR_WARNING:
-                return getBlurWarningDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_WARNING:
-                return getFocusWarningDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_AE_WARNING:
-                return getAutoExposureWarningDescription();
-            default:
-                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 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:
-                return "Auto";
-            case 1:
-                return "Portrait scene";
-            case 2:
-                return "Landscape scene";
-            case 4:
-                return "Sports scene";
-            case 5:
-                return "Night scene";
-            case 6:
-                return "Program AE";
-            case 256:
-                return "Aperture priority AE";
-            case 512:
-                return "Shutter priority AE";
-            case 768:
-                return "Manual exposure";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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:
-                return "Off";
-            case 1:
-                return "On";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-}
Index: unk/src/com/drew/metadata/exif/FujifilmMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/FujifilmMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,95 +1,0 @@
-/*
- * 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 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; // 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
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        _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");
-    }
-
-    public FujifilmMakernoteDirectory()
-    {
-        this.setDescriptor(new FujifilmMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "FujiFilm Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: /trunk/src/com/drew/metadata/exif/GpsDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
@@ -29,8 +29,10 @@
 import java.text.DecimalFormat;
 
+import static com.drew.metadata.exif.GpsDirectory.*;
+
 /**
- * Provides human-readable string representations of tag values stored in a <code>GpsDirectory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
+ * Provides human-readable string representations of tag values stored in a {@link GpsDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class GpsDescriptor extends TagDescriptor<GpsDirectory>
@@ -41,39 +43,40 @@
     }
 
+    @Override
     @Nullable
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case GpsDirectory.TAG_GPS_VERSION_ID:
+            case TAG_VERSION_ID:
                 return getGpsVersionIdDescription();
-            case GpsDirectory.TAG_GPS_ALTITUDE:
+            case TAG_ALTITUDE:
                 return getGpsAltitudeDescription();
-            case GpsDirectory.TAG_GPS_ALTITUDE_REF:
+            case TAG_ALTITUDE_REF:
                 return getGpsAltitudeRefDescription();
-            case GpsDirectory.TAG_GPS_STATUS:
+            case TAG_STATUS:
                 return getGpsStatusDescription();
-            case GpsDirectory.TAG_GPS_MEASURE_MODE:
+            case TAG_MEASURE_MODE:
                 return getGpsMeasureModeDescription();
-            case GpsDirectory.TAG_GPS_SPEED_REF:
+            case TAG_SPEED_REF:
                 return getGpsSpeedRefDescription();
-            case GpsDirectory.TAG_GPS_TRACK_REF:
-            case GpsDirectory.TAG_GPS_IMG_DIRECTION_REF:
-            case GpsDirectory.TAG_GPS_DEST_BEARING_REF:
+            case TAG_TRACK_REF:
+            case TAG_IMG_DIRECTION_REF:
+            case TAG_DEST_BEARING_REF:
                 return getGpsDirectionReferenceDescription(tagType);
-            case GpsDirectory.TAG_GPS_TRACK:
-            case GpsDirectory.TAG_GPS_IMG_DIRECTION:
-            case GpsDirectory.TAG_GPS_DEST_BEARING:
+            case TAG_TRACK:
+            case TAG_IMG_DIRECTION:
+            case TAG_DEST_BEARING:
                 return getGpsDirectionDescription(tagType);
-            case GpsDirectory.TAG_GPS_DEST_DISTANCE_REF:
+            case TAG_DEST_DISTANCE_REF:
                 return getGpsDestinationReferenceDescription();
-            case GpsDirectory.TAG_GPS_TIME_STAMP:
+            case TAG_TIME_STAMP:
                 return getGpsTimeStampDescription();
-            case GpsDirectory.TAG_GPS_LONGITUDE:
+            case TAG_LONGITUDE:
                 // three rational numbers -- displayed in HH"MM"SS.ss
                 return getGpsLongitudeDescription();
-            case GpsDirectory.TAG_GPS_LATITUDE:
+            case TAG_LATITUDE:
                 // three rational numbers -- displayed in HH"MM"SS.ss
                 return getGpsLatitudeDescription();
-            case GpsDirectory.TAG_GPS_DIFFERENTIAL:
+            case TAG_DIFFERENTIAL:
                 return getGpsDifferentialDescription();
             default:
@@ -85,5 +88,5 @@
     private String getGpsVersionIdDescription()
     {
-        return convertBytesToVersionString(_directory.getIntArray(GpsDirectory.TAG_GPS_VERSION_ID), 1);
+        return getVersionBytesDescription(TAG_VERSION_ID, 1);
     }
 
@@ -92,9 +95,5 @@
     {
         GeoLocation location = _directory.getGeoLocation();
-
-        if (location == null)
-            return null;
-
-        return GeoLocation.decimalToDegreesMinutesSecondsString(location.getLatitude());
+        return location == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(location.getLatitude());
     }
 
@@ -103,9 +102,5 @@
     {
         GeoLocation location = _directory.getGeoLocation();
-
-        if (location == null)
-            return null;
-
-        return GeoLocation.decimalToDegreesMinutesSecondsString(location.getLongitude());
+        return location == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(location.getLongitude());
     }
 
@@ -114,15 +109,6 @@
     {
         // time in hour, min, sec
-        int[] timeComponents = _directory.getIntArray(GpsDirectory.TAG_GPS_TIME_STAMP);
-        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();
+        int[] timeComponents = _directory.getIntArray(TAG_TIME_STAMP);
+        return timeComponents == null ? null : String.format("%d:%d:%d UTC", timeComponents[0], timeComponents[1], timeComponents[2]);
     }
 
@@ -130,6 +116,6 @@
     public String getGpsDestinationReferenceDescription()
     {
-        final String value = _directory.getString(GpsDirectory.TAG_GPS_DEST_DISTANCE_REF);
-        if (value==null)
+        final String value = _directory.getString(TAG_DEST_DISTANCE_REF);
+        if (value == null)
             return null;
         String distanceRef = value.trim();
@@ -151,9 +137,7 @@
         // 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";
+            ? new DecimalFormat("0.##").format(angle.doubleValue())
+            : _directory.getString(tagType);
+        return value == null || value.trim().length() == 0 ? null : value.trim() + " degrees";
     }
 
@@ -162,5 +146,5 @@
     {
         final String value = _directory.getString(tagType);
-        if (value==null)
+        if (value == null)
             return null;
         String gpsDistRef = value.trim();
@@ -177,6 +161,6 @@
     public String getGpsSpeedRefDescription()
     {
-        final String value = _directory.getString(GpsDirectory.TAG_GPS_SPEED_REF);
-        if (value==null)
+        final String value = _directory.getString(TAG_SPEED_REF);
+        if (value == null)
             return null;
         String gpsSpeedRef = value.trim();
@@ -195,6 +179,6 @@
     public String getGpsMeasureModeDescription()
     {
-        final String value = _directory.getString(GpsDirectory.TAG_GPS_MEASURE_MODE);
-        if (value==null)
+        final String value = _directory.getString(TAG_MEASURE_MODE);
+        if (value == null)
             return null;
         String gpsSpeedMeasureMode = value.trim();
@@ -211,6 +195,6 @@
     public String getGpsStatusDescription()
     {
-        final String value = _directory.getString(GpsDirectory.TAG_GPS_STATUS);
-        if (value==null)
+        final String value = _directory.getString(TAG_STATUS);
+        if (value == null)
             return null;
         String gpsStatus = value.trim();
@@ -227,12 +211,5 @@
     public String getGpsAltitudeRefDescription()
     {
-        Integer value = _directory.getInteger(GpsDirectory.TAG_GPS_ALTITUDE_REF);
-        if (value==null)
-            return null;
-        if (value == 0)
-            return "Sea level";
-        if (value == 1)
-            return "Below sea level";
-        return "Unknown (" + value + ")";
+        return getIndexedDescription(TAG_ALTITUDE_REF, "Sea level", "Below sea level");
     }
 
@@ -240,8 +217,6 @@
     public String getGpsAltitudeDescription()
     {
-        final Rational value = _directory.getRational(GpsDirectory.TAG_GPS_ALTITUDE);
-        if (value==null)
-            return null;
-        return value.intValue() + " metres";
+        final Rational value = _directory.getRational(TAG_ALTITUDE);
+        return value == null ? null : value.intValue() + " metres";
     }
 
@@ -249,12 +224,5 @@
     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 + ")";
+        return getIndexedDescription(TAG_DIFFERENTIAL, "No Correction", "Differential Corrected");
     }
 
@@ -263,9 +231,5 @@
     {
         GeoLocation location = _directory.getGeoLocation();
-
-        if (location == null)
-            return null;
-
-        return location.toDMSString();
+        return location == null ? null : location.toDMSString();
     }
 }
Index: /trunk/src/com/drew/metadata/exif/GpsDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.exif;
@@ -32,68 +32,68 @@
  * Describes Exif tags that contain Global Positioning System (GPS) data.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class GpsDirectory extends Directory
 {
     /** GPS tag version GPSVersionID 0 0 BYTE 4 */
-    public static final int TAG_GPS_VERSION_ID = 0x0000;
+    public static final int TAG_VERSION_ID = 0x0000;
     /** North or South Latitude GPSLatitudeRef 1 1 ASCII 2 */
-    public static final int TAG_GPS_LATITUDE_REF = 0x0001;
+    public static final int TAG_LATITUDE_REF = 0x0001;
     /** Latitude GPSLatitude 2 2 RATIONAL 3 */
-    public static final int TAG_GPS_LATITUDE = 0x0002;
+    public static final int TAG_LATITUDE = 0x0002;
     /** East or West Longitude GPSLongitudeRef 3 3 ASCII 2 */
-    public static final int TAG_GPS_LONGITUDE_REF = 0x0003;
+    public static final int TAG_LONGITUDE_REF = 0x0003;
     /** Longitude GPSLongitude 4 4 RATIONAL 3 */
-    public static final int TAG_GPS_LONGITUDE = 0x0004;
+    public static final int TAG_LONGITUDE = 0x0004;
     /** Altitude reference GPSAltitudeRef 5 5 BYTE 1 */
-    public static final int TAG_GPS_ALTITUDE_REF = 0x0005;
+    public static final int TAG_ALTITUDE_REF = 0x0005;
     /** Altitude GPSAltitude 6 6 RATIONAL 1 */
-    public static final int TAG_GPS_ALTITUDE = 0x0006;
+    public static final int TAG_ALTITUDE = 0x0006;
     /** GPS time (atomic clock) GPSTimeStamp 7 7 RATIONAL 3 */
-    public static final int TAG_GPS_TIME_STAMP = 0x0007;
+    public static final int TAG_TIME_STAMP = 0x0007;
     /** GPS satellites used for measurement GPSSatellites 8 8 ASCII Any */
-    public static final int TAG_GPS_SATELLITES = 0x0008;
+    public static final int TAG_SATELLITES = 0x0008;
     /** GPS receiver status GPSStatus 9 9 ASCII 2 */
-    public static final int TAG_GPS_STATUS = 0x0009;
+    public static final int TAG_STATUS = 0x0009;
     /** GPS measurement mode GPSMeasureMode 10 A ASCII 2 */
-    public static final int TAG_GPS_MEASURE_MODE = 0x000A;
+    public static final int TAG_MEASURE_MODE = 0x000A;
     /** Measurement precision GPSDOP 11 B RATIONAL 1 */
-    public static final int TAG_GPS_DOP = 0x000B;
+    public static final int TAG_DOP = 0x000B;
     /** Speed unit GPSSpeedRef 12 C ASCII 2 */
-    public static final int TAG_GPS_SPEED_REF = 0x000C;
+    public static final int TAG_SPEED_REF = 0x000C;
     /** Speed of GPS receiver GPSSpeed 13 D RATIONAL 1 */
-    public static final int TAG_GPS_SPEED = 0x000D;
+    public static final int TAG_SPEED = 0x000D;
     /** Reference for direction of movement GPSTrackRef 14 E ASCII 2 */
-    public static final int TAG_GPS_TRACK_REF = 0x000E;
+    public static final int TAG_TRACK_REF = 0x000E;
     /** Direction of movement GPSTrack 15 F RATIONAL 1 */
-    public static final int TAG_GPS_TRACK = 0x000F;
+    public static final int TAG_TRACK = 0x000F;
     /** Reference for direction of image GPSImgDirectionRef 16 10 ASCII 2 */
-    public static final int TAG_GPS_IMG_DIRECTION_REF = 0x0010;
+    public static final int TAG_IMG_DIRECTION_REF = 0x0010;
     /** Direction of image GPSImgDirection 17 11 RATIONAL 1 */
-    public static final int TAG_GPS_IMG_DIRECTION = 0x0011;
+    public static final int TAG_IMG_DIRECTION = 0x0011;
     /** Geodetic survey data used GPSMapDatum 18 12 ASCII Any */
-    public static final int TAG_GPS_MAP_DATUM = 0x0012;
+    public static final int TAG_MAP_DATUM = 0x0012;
     /** Reference for latitude of destination GPSDestLatitudeRef 19 13 ASCII 2 */
-    public static final int TAG_GPS_DEST_LATITUDE_REF = 0x0013;
+    public static final int TAG_DEST_LATITUDE_REF = 0x0013;
     /** Latitude of destination GPSDestLatitude 20 14 RATIONAL 3 */
-    public static final int TAG_GPS_DEST_LATITUDE = 0x0014;
+    public static final int TAG_DEST_LATITUDE = 0x0014;
     /** Reference for longitude of destination GPSDestLongitudeRef 21 15 ASCII 2 */
-    public static final int TAG_GPS_DEST_LONGITUDE_REF = 0x0015;
+    public static final int TAG_DEST_LONGITUDE_REF = 0x0015;
     /** Longitude of destination GPSDestLongitude 22 16 RATIONAL 3 */
-    public static final int TAG_GPS_DEST_LONGITUDE = 0x0016;
+    public static final int TAG_DEST_LONGITUDE = 0x0016;
     /** Reference for bearing of destination GPSDestBearingRef 23 17 ASCII 2 */
-    public static final int TAG_GPS_DEST_BEARING_REF = 0x0017;
+    public static final int TAG_DEST_BEARING_REF = 0x0017;
     /** Bearing of destination GPSDestBearing 24 18 RATIONAL 1 */
-    public static final int TAG_GPS_DEST_BEARING = 0x0018;
+    public static final int TAG_DEST_BEARING = 0x0018;
     /** Reference for distance to destination GPSDestDistanceRef 25 19 ASCII 2 */
-    public static final int TAG_GPS_DEST_DISTANCE_REF = 0x0019;
+    public static final int TAG_DEST_DISTANCE_REF = 0x0019;
     /** Distance to destination GPSDestDistance 26 1A RATIONAL 1 */
-    public static final int TAG_GPS_DEST_DISTANCE = 0x001A;
+    public static final int TAG_DEST_DISTANCE = 0x001A;
 
     /** 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;
+    public static final int TAG_PROCESSING_METHOD = 0x001B;
+    public static final int TAG_AREA_INFORMATION = 0x001C;
+    public static final int TAG_DATE_STAMP = 0x001D;
+    public static final int TAG_DIFFERENTIAL = 0x001E;
 
     @NotNull
@@ -102,35 +102,35 @@
     static
     {
-        _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");
+        _tagNameMap.put(TAG_VERSION_ID, "GPS Version ID");
+        _tagNameMap.put(TAG_LATITUDE_REF, "GPS Latitude Ref");
+        _tagNameMap.put(TAG_LATITUDE, "GPS Latitude");
+        _tagNameMap.put(TAG_LONGITUDE_REF, "GPS Longitude Ref");
+        _tagNameMap.put(TAG_LONGITUDE, "GPS Longitude");
+        _tagNameMap.put(TAG_ALTITUDE_REF, "GPS Altitude Ref");
+        _tagNameMap.put(TAG_ALTITUDE, "GPS Altitude");
+        _tagNameMap.put(TAG_TIME_STAMP, "GPS Time-Stamp");
+        _tagNameMap.put(TAG_SATELLITES, "GPS Satellites");
+        _tagNameMap.put(TAG_STATUS, "GPS Status");
+        _tagNameMap.put(TAG_MEASURE_MODE, "GPS Measure Mode");
+        _tagNameMap.put(TAG_DOP, "GPS DOP");
+        _tagNameMap.put(TAG_SPEED_REF, "GPS Speed Ref");
+        _tagNameMap.put(TAG_SPEED, "GPS Speed");
+        _tagNameMap.put(TAG_TRACK_REF, "GPS Track Ref");
+        _tagNameMap.put(TAG_TRACK, "GPS Track");
+        _tagNameMap.put(TAG_IMG_DIRECTION_REF, "GPS Img Direction Ref");
+        _tagNameMap.put(TAG_IMG_DIRECTION, "GPS Img Direction");
+        _tagNameMap.put(TAG_MAP_DATUM, "GPS Map Datum");
+        _tagNameMap.put(TAG_DEST_LATITUDE_REF, "GPS Dest Latitude Ref");
+        _tagNameMap.put(TAG_DEST_LATITUDE, "GPS Dest Latitude");
+        _tagNameMap.put(TAG_DEST_LONGITUDE_REF, "GPS Dest Longitude Ref");
+        _tagNameMap.put(TAG_DEST_LONGITUDE, "GPS Dest Longitude");
+        _tagNameMap.put(TAG_DEST_BEARING_REF, "GPS Dest Bearing Ref");
+        _tagNameMap.put(TAG_DEST_BEARING, "GPS Dest Bearing");
+        _tagNameMap.put(TAG_DEST_DISTANCE_REF, "GPS Dest Distance Ref");
+        _tagNameMap.put(TAG_DEST_DISTANCE, "GPS Dest Distance");
+        _tagNameMap.put(TAG_PROCESSING_METHOD, "GPS Processing Method");
+        _tagNameMap.put(TAG_AREA_INFORMATION, "GPS Area Information");
+        _tagNameMap.put(TAG_DATE_STAMP, "GPS Date Stamp");
+        _tagNameMap.put(TAG_DIFFERENTIAL, "GPS Differential");
     }
 
@@ -140,4 +140,5 @@
     }
 
+    @Override
     @NotNull
     public String getName()
@@ -146,4 +147,5 @@
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
@@ -161,8 +163,8 @@
     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);
+        Rational[] latitudes = getRationalArray(GpsDirectory.TAG_LATITUDE);
+        Rational[] longitudes = getRationalArray(GpsDirectory.TAG_LONGITUDE);
+        String latitudeRef = getString(GpsDirectory.TAG_LATITUDE_REF);
+        String longitudeRef = getString(GpsDirectory.TAG_LONGITUDE_REF);
 
         // Make sure we have the required values
Index: unk/src/com/drew/metadata/exif/KodakMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/KodakMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,39 +1,0 @@
-/*
- * 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>KodakMakernoteDirectory</code>.
- * 
- * Thanks to David Carson for the initial version of this class.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory>
-{
-    public KodakMakernoteDescriptor(@NotNull KodakMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-}
Index: unk/src/com/drew/metadata/exif/KodakMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/KodakMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,54 +1,0 @@
-/*
- * 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 Kodak cameras.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class KodakMakernoteDirectory extends Directory
-{
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    public KodakMakernoteDirectory()
-    {
-        this.setDescriptor(new KodakMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Kodak Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/KyoceraMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/KyoceraMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,76 +1,0 @@
-/*
- * 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>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<KyoceraMakernoteDirectory>
-{
-    public KyoceraMakernoteDescriptor(@NotNull KyoceraMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case KyoceraMakernoteDirectory.TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
-            case KyoceraMakernoteDirectory.TAG_KYOCERA_PROPRIETARY_THUMBNAIL:
-                return getProprietaryThumbnailDataDescription();
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    @Nullable
-    public String getPrintImageMatchingInfoDescription()
-    {
-        byte[] bytes = _directory.getByteArray(KyoceraMakernoteDirectory.TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO);
-        if (bytes==null)
-            return null;
-        return "(" + bytes.length + " bytes)";
-    }
-
-    @Nullable
-    public String getProprietaryThumbnailDataDescription()
-    {
-        byte[] bytes = _directory.getByteArray(KyoceraMakernoteDirectory.TAG_KYOCERA_PROPRIETARY_THUMBNAIL);
-        if (bytes==null)
-            return null;
-        return "(" + bytes.length + " bytes)";
-    }
-}
Index: unk/src/com/drew/metadata/exif/KyoceraMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/KyoceraMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,63 +1,0 @@
-/*
- * 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 Kyocera and Contax cameras.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class KyoceraMakernoteDirectory extends Directory
-{
-    public static final int TAG_KYOCERA_PROPRIETARY_THUMBNAIL = 0x0001;
-    public static final int TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        _tagNameMap.put(TAG_KYOCERA_PROPRIETARY_THUMBNAIL, "Proprietary Thumbnail Format Data");
-        _tagNameMap.put(TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
-    }
-
-    public KyoceraMakernoteDirectory()
-    {
-        this.setDescriptor(new KyoceraMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Kyocera/Contax Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/NikonType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/NikonType1MakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,223 +1,0 @@
-/*
- * 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>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
- * structure is shown below.
- * <pre><code>
- * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
- * :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<NikonType1MakernoteDirectory>
-{
-    public NikonType1MakernoteDescriptor(@NotNull NikonType1MakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_QUALITY:
-                return getQualityDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_COLOR_MODE:
-                return getColorModeDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT:
-                return getImageAdjustmentDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CCD_SENSITIVITY:
-                return getCcdSensitivityDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_WHITE_BALANCE:
-                return getWhiteBalanceDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_FOCUS:
-                return getFocusDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_DIGITAL_ZOOM:
-                return getDigitalZoomDescription();
-            case NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CONVERTER:
-                return getConverterDescription();
-            default:
-                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:
-                return "None";
-            case 1:
-                return "Fisheye converter";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @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";
-        }
-        return value.toSimpleString(true) + "x digital zoom";
-    }
-
-    @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";
-        }
-        return value.toSimpleString(true);
-    }
-
-    @Nullable
-    public String getWhiteBalanceDescription()
-    {
-        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_WHITE_BALANCE);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Auto";
-            case 1:
-                return "Preset";
-            case 2:
-                return "Daylight";
-            case 3:
-                return "Incandescence";
-            case 4:
-                return "Florescence";
-            case 5:
-                return "Cloudy";
-            case 6:
-                return "SpeedLight";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getCcdSensitivityDescription()
-    {
-        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CCD_SENSITIVITY);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 0:
-                return "ISO80";
-            case 2:
-                return "ISO160";
-            case 4:
-                return "ISO320";
-            case 5:
-                return "ISO100";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getImageAdjustmentDescription()
-    {
-        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Normal";
-            case 1:
-                return "Bright +";
-            case 2:
-                return "Bright -";
-            case 3:
-                return "Contrast +";
-            case 4:
-                return "Contrast -";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getColorModeDescription()
-    {
-        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_COLOR_MODE);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 1:
-                return "Color";
-            case 2:
-                return "Monochrome";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getQualityDescription()
-    {
-        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_QUALITY);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 1:
-                return "VGA Basic";
-            case 2:
-                return "VGA Normal";
-            case 3:
-                return "VGA Fine";
-            case 4:
-                return "SXGA Basic";
-            case 5:
-                return "SXGA Normal";
-            case 6:
-                return "SXGA Fine";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-}
Index: unk/src/com/drew/metadata/exif/NikonType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/NikonType1MakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,90 +1,0 @@
-/*
- * 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 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
- * 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
- * structure is shown below.
- * <pre><code>
- * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
- * :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
-{
-    public static final int TAG_NIKON_TYPE1_UNKNOWN_1 = 0x0002;
-    public static final int TAG_NIKON_TYPE1_QUALITY = 0x0003;
-    public static final int TAG_NIKON_TYPE1_COLOR_MODE = 0x0004;
-    public static final int TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT = 0x0005;
-    public static final int TAG_NIKON_TYPE1_CCD_SENSITIVITY = 0x0006;
-    public static final int TAG_NIKON_TYPE1_WHITE_BALANCE = 0x0007;
-    public static final int TAG_NIKON_TYPE1_FOCUS = 0x0008;
-    public static final int TAG_NIKON_TYPE1_UNKNOWN_2 = 0x0009;
-    public static final int TAG_NIKON_TYPE1_DIGITAL_ZOOM = 0x000A;
-    public static final int TAG_NIKON_TYPE1_CONVERTER = 0x000B;
-    public static final int TAG_NIKON_TYPE1_UNKNOWN_3 = 0x0F00;
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        _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");
-    }
-
-    public NikonType1MakernoteDirectory()
-    {
-        this.setDescriptor(new NikonType1MakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Nikon Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/NikonType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/NikonType2MakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,423 +1,0 @@
-/*
- * 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.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 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<NikonType2MakernoteDirectory>
-{
-    public NikonType2MakernoteDescriptor(@NotNull NikonType2MakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @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();
-            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT:
-                return getHueAdjustmentDescription();
-            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_COLOR_MODE:
-                return getColorModeDescription();
-            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 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 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) + ")";
-        }
-        switch (values[1]) {
-            case 0:
-                return "Centre";
-            case 1:
-                return "Top";
-            case 2:
-                return "Bottom";
-            case 3:
-                return "Left";
-            case 4:
-                return "Right";
-            default:
-                return "Unknown (" + values[1] + ")";
-        }
-    }
-
-    @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==null)
-            return null;
-        if (values[0] != 0 || values[1] == 0)
-            return "Unknown (" + _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ISO_1) + ")";
-        return "ISO " + values[1];
-    }
-
-    @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);
-
-        StringBuilder description = new StringBuilder();
-        description.append(values[0].intValue());
-        description.append('-');
-        description.append(values[1].intValue());
-        description.append("mm f/");
-        description.append(values[2].floatValue());
-        description.append('-');
-        description.append(values[3].floatValue());
-
-        return description.toString();
-    }
-
-    @Nullable
-    public String getHueAdjustmentDescription()
-    {
-        final String value = _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT);
-        if (value==null)
-            return null;
-        return value + " degrees";
-    }
-
-    @Nullable
-    public String getColorModeDescription()
-    {
-        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 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: unk/src/com/drew/metadata/exif/NikonType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/NikonType2MakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,922 +1,0 @@
-/*
- * 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 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
-{
-    /**
-     * Values observed
-     * - 0200 (D70)
-     * - 0200 (D1X)
-     */
-    public static final int TAG_NIKON_TYPE2_FIRMWARE_VERSION = 0x0001;
-
-    /**
-     * Values observed
-     * - 0 250
-     * - 0 400
-     */
-    public static final int TAG_NIKON_TYPE2_ISO_1 = 0x0002;
-
-    /**
-     * 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;
-
-    /**
-     * 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 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 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 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;
-
-    /**
-     * 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;
-
-    /**
-     * 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;
-
-    /**
-     * 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;
-
-    /**
-     * 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;
-
-    /**
-     * The first two numbers are coefficients to multiply red and blue channels according to white balance as set in the
-     * camera. The meaning of the third and the fourth numbers is unknown.
-     *
-     * Values observed
-     * - 2.25882352 1.76078431 0.0 0.0
-     * - 10242/1 34305/1 0/1 0/1
-     * - 234765625/100000000 1140625/1000000 1/1 1/1
-     */
-    public static final int TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE_RB_COEFF = 0x000C;
-
-    /**
-     * 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_DATA_DUMP = 0x0010;
-
-    /**
-     * 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;
-
-    /**
-     * 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)
-     * <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 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;
-
-    /**
-     * 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;
-
-    /**
-     * 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;
-
-    /**
-     * A pair of focal/max-fstop values that describe the lens used.
-     *
-     * Values observed
-     * - 180.0,180.0,2.8,2.8 (D100)
-     * - 240/10 850/10 35/10 45/10
-     * - 18-70mm f/3.5-4.5 (D70)
-     * - 17-35mm f/2.8-2.8 (D1X)
-     * - 70-200mm f/2.8-2.8 (D70)
-     *
-     * Nikon Browser identifies the lens as "18-70mm F/3.5-4.5 G" which
-     * is identical to metadata extractor, except for the "G".  This must
-     * be coming from another tag...
-     */
-    public static final int TAG_NIKON_TYPE2_LENS = 0x0084;
-
-    /**
-     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
-     */
-    public static final int TAG_NIKON_TYPE2_MANUAL_FOCUS_DISTANCE = 0x0085;
-
-    /**
-     * 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
-     */
-    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(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");
-    }
-
-    public NikonType2MakernoteDirectory()
-    {
-        this.setDescriptor(new NikonType2MakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Nikon Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/OlympusMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/OlympusMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,173 +1,0 @@
-/*
- * 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>OlympusMakernoteDirectory</code>.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDirectory>
-{
-    public OlympusMakernoteDescriptor(@NotNull OlympusMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case OlympusMakernoteDirectory.TAG_OLYMPUS_SPECIAL_MODE:
-                return getSpecialModeDescription();
-            case OlympusMakernoteDirectory.TAG_OLYMPUS_JPEG_QUALITY:
-                return getJpegQualityDescription();
-            case OlympusMakernoteDirectory.TAG_OLYMPUS_MACRO_MODE:
-                return getMacroModeDescription();
-            case OlympusMakernoteDirectory.TAG_OLYMPUS_DIGI_ZOOM_RATIO:
-                return getDigiZoomRatioDescription();
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    @Nullable
-    public String getDigiZoomRatioDescription()
-    {
-        Integer value = _directory.getInteger(OlympusMakernoteDirectory.TAG_OLYMPUS_DIGI_ZOOM_RATIO);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Normal";
-            case 2:
-                return "Digital 2x Zoom";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getMacroModeDescription()
-    {
-        Integer value = _directory.getInteger(OlympusMakernoteDirectory.TAG_OLYMPUS_MACRO_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0:
-                return "Normal (no macro)";
-            case 1:
-                return "Macro";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getJpegQualityDescription()
-    {
-        Integer value = _directory.getInteger(OlympusMakernoteDirectory.TAG_OLYMPUS_JPEG_QUALITY);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "SQ";
-            case 2:
-                return "HQ";
-            case 3:
-                return "SHQ";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSpecialModeDescription()
-    {
-        int[] values = _directory.getIntArray(OlympusMakernoteDirectory.TAG_OLYMPUS_SPECIAL_MODE);
-        if (values==null)
-            return null;
-        if (values.length < 1)
-            return "";
-        StringBuilder desc = new StringBuilder();
-        switch (values[0]) {
-            case 0:
-                desc.append("Normal picture taking mode");
-                break;
-            case 1:
-                desc.append("Unknown picture taking mode");
-                break;
-            case 2:
-                desc.append("Fast picture taking mode");
-                break;
-            case 3:
-                desc.append("Panorama picture taking mode");
-                break;
-            default:
-                desc.append("Unknown picture taking mode");
-                break;
-        }
-
-        if (values.length < 2)
-            return desc.toString();
-        desc.append(" - ");
-        switch (values[1]) {
-            case 0:
-                desc.append("Unknown sequence number");
-                break;
-            case 1:
-                desc.append("1st in a sequence");
-                break;
-            case 2:
-                desc.append("2nd in a sequence");
-                break;
-            case 3:
-                desc.append("3rd in a sequence");
-                break;
-            default:
-                desc.append(values[1]);
-                desc.append("th in a sequence");
-                break;
-        }
-        if (values.length < 3)
-            return desc.toString();
-        desc.append(" - ");
-        switch (values[2]) {
-            case 1:
-                desc.append("Left to right panorama direction");
-                break;
-            case 2:
-                desc.append("Right to left panorama direction");
-                break;
-            case 3:
-                desc.append("Bottom to top panorama direction");
-                break;
-            case 4:
-                desc.append("Top to bottom panorama direction");
-                break;
-        }
-        return desc.toString();
-    }
-}
Index: unk/src/com/drew/metadata/exif/OlympusMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/OlympusMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,232 +1,0 @@
-/*
- * 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;
-
-/**
- * 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. */
-    public static final int TAG_OLYMPUS_MAKERNOTE_VERSION = 0x0000;
-    /** Used by Konica / Minolta cameras. */
-    public static final int TAG_OLYMPUS_CAMERA_SETTINGS_1 = 0x0001;
-    /** Alternate Camera Settings Tag. Used by Konica / Minolta cameras. */
-    public static final int TAG_OLYMPUS_CAMERA_SETTINGS_2 = 0x0003;
-    /** Used by Konica / Minolta cameras. */
-    public static final int TAG_OLYMPUS_COMPRESSED_IMAGE_SIZE = 0x0040;
-    /** Used by Konica / Minolta cameras. */
-    public static final int TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_1 = 0x0081;
-    /** 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. */
-    public static final int TAG_OLYMPUS_MINOLTA_THUMBNAIL_LENGTH = 0x0089;
-
-    /**
-     * Used by Konica / Minolta cameras
-     * 0 = Natural Colour
-     * 1 = Black & White
-     * 2 = Vivid colour
-     * 3 = Solarization
-     * 4 = AdobeRGB
-     */
-    public static final int TAG_OLYMPUS_COLOUR_MODE = 0x0101;
-
-    /**
-     * Used by Konica / Minolta cameras.
-     * 0 = Raw
-     * 1 = Super Fine
-     * 2 = Fine
-     * 3 = Standard
-     * 4 = Extra Fine
-     */
-    public static final int TAG_OLYMPUS_IMAGE_QUALITY_1 = 0x0102;
-
-    /**
-     * Not 100% sure about this tag.
-     * <p/>
-     * Used by Konica / Minolta cameras.
-     * 0 = Raw
-     * 1 = Super Fine
-     * 2 = Fine
-     * 3 = Standard
-     * 4 = Extra Fine
-     */
-    public static final int TAG_OLYMPUS_IMAGE_QUALITY_2 = 0x0103;
-
-
-    /**
-     * Three values:
-     * Value 1: 0=Normal, 2=Fast, 3=Panorama
-     * Value 2: Sequence Number Value 3:
-     * 1 = Panorama Direction: Left to Right
-     * 2 = Panorama Direction: Right to Left
-     * 3 = Panorama Direction: Bottom to Top
-     * 4 = Panorama Direction: Top to Bottom
-     */
-    public static final int TAG_OLYMPUS_SPECIAL_MODE = 0x0200;
-
-    /**
-     * 1 = Standard Quality
-     * 2 = High Quality
-     * 3 = Super High Quality
-     */
-    public static final int TAG_OLYMPUS_JPEG_QUALITY = 0x0201;
-
-    /**
-     * 0 = Normal (Not Macro)
-     * 1 = Macro
-     */
-    public static final int TAG_OLYMPUS_MACRO_MODE = 0x0202;
-
-    public static final int TAG_OLYMPUS_UNKNOWN_1 = 0x0203;
-
-    /** 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;
-
-    /**
-     * Used by Epson cameras
-     * Units = pixels
-     */
-    public static final int TAG_OLYMPUS_IMAGE_WIDTH = 0x020B;
-
-    /**
-     * Used by Epson cameras
-     * Units = pixels
-     */
-    public static final int TAG_OLYMPUS_IMAGE_HEIGHT = 0x020C;
-
-    /** A string. Used by Epson cameras. */
-    public static final int TAG_OLYMPUS_ORIGINAL_MANUFACTURER_MODEL = 0x020D;
-
-    /**
-     * See the PIM specification here:
-     * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-     */
-    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;
-
-    @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");
-    }
-
-    public OlympusMakernoteDirectory()
-    {
-        this.setDescriptor(new OlympusMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Olympus Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/PanasonicMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,1103 +1,0 @@
-/*
- * 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.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 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>
- *
- * @author Drew Noakes http://drewnoakes.com, Philipp Sandhaus
- */
-public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory>
-{
-    public PanasonicMakernoteDescriptor(@NotNull PanasonicMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @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
-        {
-            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";
-            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";
-            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: unk/src/com/drew/metadata/exif/PanasonicMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,642 +1,0 @@
-/*
- * 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.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
-{
-
-    /**
-     * <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(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");
-    }
-
-    public PanasonicMakernoteDirectory()
-    {
-        this.setDescriptor(new PanasonicMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Panasonic Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        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: unk/src/com/drew/metadata/exif/PentaxMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PentaxMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,282 +1,0 @@
-/*
- * 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>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<PentaxMakernoteDirectory>
-{
-    public PentaxMakernoteDescriptor(@NotNull PentaxMakernoteDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) 
-        {
-            case PentaxMakernoteDirectory.TAG_PENTAX_CAPTURE_MODE:
-                return getCaptureModeDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_QUALITY_LEVEL:
-                return getQualityLevelDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_FOCUS_MODE:
-                return getFocusModeDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_FLASH_MODE:
-                return getFlashModeDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_WHITE_BALANCE:
-                return getWhiteBalanceDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_DIGITAL_ZOOM:
-                return getDigitalZoomDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_SHARPNESS:
-                return getSharpnessDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_CONTRAST:
-                return getContrastDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_SATURATION:
-                return getSaturationDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_ISO_SPEED:
-                return getIsoSpeedDescription();
-            case PentaxMakernoteDirectory.TAG_PENTAX_COLOUR:
-                return getColourDescription();
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    @Nullable
-    public String getColourDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_COLOUR);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 1:  return "Normal";
-            case 2:  return "Black & White";
-            case 3:  return "Sepia";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getIsoSpeedDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_ISO_SPEED);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            // TODO there must be other values which aren't catered for here
-            case 10:  return "ISO 100";
-            case 16:  return "ISO 200";
-            case 100: return "ISO 100";
-            case 200: return "ISO 200";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSaturationDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_SATURATION);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 0:  return "Normal";
-            case 1:  return "Low";
-            case 2:  return "High";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getContrastDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_CONTRAST);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 0:  return "Normal";
-            case 1:  return "Low";
-            case 2:  return "High";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getSharpnessDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_SHARPNESS);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 0:  return "Normal";
-            case 1:  return "Soft";
-            case 2:  return "Hard";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getDigitalZoomDescription()
-    {
-        Float value = _directory.getFloatObject(PentaxMakernoteDirectory.TAG_PENTAX_DIGITAL_ZOOM);
-        if (value==null)
-            return null;
-        if (value==0)
-            return "Off";
-        return Float.toString(value);
-    }
-
-    @Nullable
-    public String getWhiteBalanceDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_WHITE_BALANCE);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 0:  return "Auto";
-            case 1:  return "Daylight";
-            case 2:  return "Shade";
-            case 3:  return "Tungsten";
-            case 4:  return "Fluorescent";
-            case 5:  return "Manual";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFlashModeDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_FLASH_MODE);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 1:  return "Auto";
-            case 2:  return "Flash On";
-            case 4:  return "Flash Off";
-            case 6:  return "Red-eye Reduction";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getFocusModeDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_FOCUS_MODE);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 2:  return "Custom";
-            case 3:  return "Auto";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getQualityLevelDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_QUALITY_LEVEL);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 0:  return "Good";
-            case 1:  return "Better";
-            case 2:  return "Best";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getCaptureModeDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_CAPTURE_MODE);
-        if (value==null)
-            return null;
-        switch (value)
-        {
-            case 1:  return "Auto";
-            case 2:  return "Night-scene";
-            case 3:  return "Manual";
-            case 4:  return "Multiple";
-            default: return "Unknown (" + value + ")";
-        }
-    }
-
-/*
-    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()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PANASONIC_MACRO_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "On";
-            case 2:
-                return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getRecordModeDescription()
-    {
-        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PANASONIC_RECORD_MODE);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "Normal";
-            case 2:
-                return "Portrait";
-            case 9:
-                return "Macro";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-*/
-}
Index: unk/src/com/drew/metadata/exif/PentaxMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PentaxMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,168 +1,0 @@
-/*
- * 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 Pentax and Asahi cameras.
- *
- * @author Drew Noakes http://drewnoakes.com
- */
-public class PentaxMakernoteDirectory extends Directory
-{
-    /**
-     * 0 = Auto
-     * 1 = Night-scene
-     * 2 = Manual
-     * 4 = Multiple
-     */
-    public static final int TAG_PENTAX_CAPTURE_MODE = 0x0001;
-
-    /**
-     * 0 = Good
-     * 1 = Better
-     * 2 = Best
-     */
-    public static final int TAG_PENTAX_QUALITY_LEVEL = 0x0002;
-
-    /**
-     * 2 = Custom
-     * 3 = Auto
-     */
-    public static final int TAG_PENTAX_FOCUS_MODE = 0x0003;
-
-    /**
-     * 1 = Auto
-     * 2 = Flash on
-     * 4 = Flash off
-     * 6 = Red-eye Reduction
-     */
-    public static final int TAG_PENTAX_FLASH_MODE = 0x0004;
-
-    /**
-     * 0 = Auto
-     * 1 = Daylight
-     * 2 = Shade
-     * 3 = Tungsten
-     * 4 = Fluorescent
-     * 5 = Manual
-     */
-    public static final int TAG_PENTAX_WHITE_BALANCE = 0x0007;
-
-    /**
-     * (0 = Off)
-     */
-    public static final int TAG_PENTAX_DIGITAL_ZOOM = 0x000A;
-
-    /**
-     * 0 = Normal
-     * 1 = Soft
-     * 2 = Hard
-     */
-    public static final int TAG_PENTAX_SHARPNESS = 0x000B;
-
-    /**
-     * 0 = Normal
-     * 1 = Low
-     * 2 = High
-     */
-    public static final int TAG_PENTAX_CONTRAST = 0x000C;
-
-    /**
-     * 0 = Normal
-     * 1 = Low
-     * 2 = High
-     */
-    public static final int TAG_PENTAX_SATURATION = 0x000D;
-
-    /**
-     * 10 = ISO 100
-     * 16 = ISO 200
-     * 100 = ISO 100
-     * 200 = ISO 200
-     */
-    public static final int TAG_PENTAX_ISO_SPEED = 0x0014;
-
-    /**
-     * 1 = Normal
-     * 2 = Black & White
-     * 3 = Sepia
-     */
-    public static final int TAG_PENTAX_COLOUR = 0x0017;
-
-    /**
-     * See Print Image Matching for specification.
-     * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-     */
-    public static final int TAG_PENTAX_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
-
-    /**
-     * (String).
-     */
-    public static final int TAG_PENTAX_TIME_ZONE = 0x1000;
-
-    /**
-     * (String).
-     */
-    public static final int TAG_PENTAX_DAYLIGHT_SAVINGS = 0x1001;
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static
-    {
-        _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");
-    }
-
-    public PentaxMakernoteDirectory()
-    {
-        this.setDescriptor(new PentaxMakernoteDescriptor(this));
-    }
-
-    @NotNull
-    public String getName()
-    {
-        return "Pentax Makernote";
-    }
-
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: unk/src/com/drew/metadata/exif/SigmaMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/SigmaMakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,38 +1,0 @@
-/*
- * 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: unk/src/com/drew/metadata/exif/SigmaMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/SigmaMakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,107 +1,0 @@
-/*
- * 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: unk/src/com/drew/metadata/exif/SonyType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/SonyType1MakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,255 +1,0 @@
-/*
- * 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: unk/src/com/drew/metadata/exif/SonyType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/SonyType1MakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,93 +1,0 @@
-/*
- * 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: unk/src/com/drew/metadata/exif/SonyType6MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/SonyType6MakernoteDescriptor.java	(revision 8131)
+++ 	(revision )
@@ -1,57 +1,0 @@
-/*
- * 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: unk/src/com/drew/metadata/exif/SonyType6MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/SonyType6MakernoteDirectory.java	(revision 8131)
+++ 	(revision )
@@ -1,68 +1,0 @@
-/*
- * 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/exif/makernotes/CanonMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,712 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link CanonMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory>
+{
+    public CanonMakernoteDescriptor(@NotNull CanonMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_CANON_SERIAL_NUMBER:
+                return getSerialNumberDescription();
+            case CameraSettings.TAG_FLASH_ACTIVITY:
+                return getFlashActivityDescription();
+            case CameraSettings.TAG_FOCUS_TYPE:
+                return getFocusTypeDescription();
+            case CameraSettings.TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case CameraSettings.TAG_QUALITY:
+                return getQualityDescription();
+            case CameraSettings.TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case CameraSettings.TAG_SELF_TIMER_DELAY:
+                return getSelfTimerDelayDescription();
+            case CameraSettings.TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case CameraSettings.TAG_CONTINUOUS_DRIVE_MODE:
+                return getContinuousDriveModeDescription();
+            case CameraSettings.TAG_FOCUS_MODE_1:
+                return getFocusMode1Description();
+            case CameraSettings.TAG_IMAGE_SIZE:
+                return getImageSizeDescription();
+            case CameraSettings.TAG_EASY_SHOOTING_MODE:
+                return getEasyShootingModeDescription();
+            case CameraSettings.TAG_CONTRAST:
+                return getContrastDescription();
+            case CameraSettings.TAG_SATURATION:
+                return getSaturationDescription();
+            case CameraSettings.TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case CameraSettings.TAG_ISO:
+                return getIsoDescription();
+            case CameraSettings.TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case CameraSettings.TAG_AF_POINT_SELECTED:
+                return getAfPointSelectedDescription();
+            case CameraSettings.TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case CameraSettings.TAG_LONG_FOCAL_LENGTH:
+                return getLongFocalLengthDescription();
+            case CameraSettings.TAG_SHORT_FOCAL_LENGTH:
+                return getShortFocalLengthDescription();
+            case CameraSettings.TAG_FOCAL_UNITS_PER_MM:
+                return getFocalUnitsPerMillimetreDescription();
+            case CameraSettings.TAG_FLASH_DETAILS:
+                return getFlashDetailsDescription();
+            case CameraSettings.TAG_FOCUS_MODE_2:
+                return getFocusMode2Description();
+            case FocalLength.TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case FocalLength.TAG_AF_POINT_USED:
+                return getAfPointUsedDescription();
+            case FocalLength.TAG_FLASH_BIAS:
+                return getFlashBiasDescription();
+
+            // 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 TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION:
+//                return getLongExposureNoiseReductionDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS:
+//                return getShutterAutoExposureLockButtonDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP:
+//                return getMirrorLockupDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL:
+//                return getTvAndAvExposureLevelDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT:
+//                return getAutoFocusAssistLightDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE:
+//                return getShutterSpeedInAvModeDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_BRACKETING:
+//                return getAutoExposureBracketingSequenceAndAutoCancellationDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC:
+//                return getShutterCurtainSyncDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_AF_STOP:
+//                return getLensAutoFocusStopButtonDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION:
+//                return getFillFlashReductionDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN:
+//                return getMenuButtonReturnPositionDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION:
+//                return getSetButtonFunctionWhenShootingDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING:
+//                return getSensorCleaningDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSerialNumberDescription()
+    {
+        Integer value = _directory.getInteger(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(TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Off";
+            case 1:     return "On";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShutterAutoExposureLockButtonDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "AF/AE lock";
+            case 1:     return "AE lock/AF";
+            case 2:     return "AF/AF lock";
+            case 3:     return "AE+release/AE+AF";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMirrorLockupDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Disabled";
+            case 1:     return "Enabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getTvAndAvExposureLevelDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "1/2 stop";
+            case 1:     return "1/3 stop";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoFocusAssistLightDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "On (Auto)";
+            case 1:     return "Off";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShutterSpeedInAvModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Automatic";
+            case 1:     return "1/200 (fixed)";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoExposureBracketingSequenceAndAutoCancellationDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_BRACKETING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "0,-,+ / Enabled";
+            case 1:     return "0,-,+ / Disabled";
+            case 2:     return "-,0,+ / Enabled";
+            case 3:     return "-,0,+ / Disabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShutterCurtainSyncDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "1st Curtain Sync";
+            case 1:     return "2nd Curtain Sync";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getLensAutoFocusStopButtonDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_AF_STOP);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "AF stop";
+            case 1:     return "Operate AF";
+            case 2:     return "Lock AE and start timer";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFillFlashReductionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Enabled";
+            case 1:     return "Disabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMenuButtonReturnPositionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Top";
+            case 1:     return "Previous (volatile)";
+            case 2:     return "Previous";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSetButtonFunctionWhenShootingDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Not Assigned";
+            case 1:     return "Change Quality";
+            case 2:     return "Change ISO Speed";
+            case 3:     return "Select Parameters";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSensorCleaningDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Disabled";
+            case 1:     return "Enabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+*/
+
+    @Nullable
+    public String getFlashBiasDescription()
+    {
+        Integer value = _directory.getInteger(FocalLength.TAG_FLASH_BIAS);
+
+        if (value == null)
+            return null;
+
+        boolean isNegative = false;
+        if (value > 0xF000) {
+            isNegative = true;
+            value = 0xFFFF - value;
+            value++;
+        }
+
+        // this tag is interesting in that the values returned are:
+        //  0, 0.375, 0.5, 0.626, 1
+        // not
+        //  0, 0.33,  0.5, 0.66,  1
+
+        return ((isNegative) ? "-" : "") + Float.toString(value / 32f) + " EV";
+    }
+
+    @Nullable
+    public String getAfPointUsedDescription()
+    {
+        Integer value = _directory.getInteger(FocalLength.TAG_AF_POINT_USED);
+        if (value == null)
+            return null;
+        if ((value & 0x7) == 0) {
+            return "Right";
+        } else if ((value & 0x7) == 1) {
+            return "Centre";
+        } else if ((value & 0x7) == 2) {
+            return "Left";
+        } else {
+            return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(
+            FocalLength.TAG_WHITE_BALANCE,
+            "Auto",
+            "Sunny",
+            "Cloudy",
+            "Tungsten",
+            "Florescent",
+            "Flash",
+            "Custom"
+        );
+    }
+
+    @Nullable
+    public String getFocusMode2Description()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_MODE_2, "Single", "Continuous");
+    }
+
+    @Nullable
+    public String getFlashDetailsDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_DETAILS);
+        if (value == null)
+            return null;
+        if (((value >> 14) & 1) > 0) {
+            return "External E-TTL";
+        }
+        if (((value >> 13) & 1) > 0) {
+            return "Internal flash";
+        }
+        if (((value >> 11) & 1) > 0) {
+            return "FP sync used";
+        }
+        if (((value >> 4) & 1) > 0) {
+            return "FP sync enabled";
+        }
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getFocalUnitsPerMillimetreDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FOCAL_UNITS_PER_MM);
+        if (value == null)
+            return null;
+        if (value != 0) {
+            return Integer.toString(value);
+        } else {
+            return "";
+        }
+    }
+
+    @Nullable
+    public String getShortFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SHORT_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        String units = getFocalUnitsPerMillimetreDescription();
+        return Integer.toString(value) + " " + units;
+    }
+
+    @Nullable
+    public String getLongFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_LONG_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        String units = getFocalUnitsPerMillimetreDescription();
+        return Integer.toString(value) + " " + units;
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_EXPOSURE_MODE,
+            "Easy shooting",
+            "Program",
+            "Tv-priority",
+            "Av-priority",
+            "Manual",
+            "A-DEP"
+        );
+    }
+
+    @Nullable
+    public String getAfPointSelectedDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_AF_POINT_SELECTED,
+            0x3000,
+            "None (MF)",
+            "Auto selected",
+            "Right",
+            "Centre",
+            "Left"
+        );
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_METERING_MODE,
+            3,
+            "Evaluative",
+            "Partial",
+            "Centre weighted"
+        );
+    }
+
+    @Nullable
+    public String getIsoDescription()
+    {
+        Integer value = _directory.getInteger(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:
+                return "Not specified (see ISOSpeedRatings tag)";
+            case 15:
+                return "Auto";
+            case 16:
+                return "50";
+            case 17:
+                return "100";
+            case 18:
+                return "200";
+            case 19:
+                return "400";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SHARPNESS);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0xFFFF:
+                return "Low";
+            case 0x000:
+                return "Normal";
+            case 0x001:
+                return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SATURATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0xFFFF:
+                return "Low";
+            case 0x000:
+                return "Normal";
+            case 0x001:
+                return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_CONTRAST);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0xFFFF:
+                return "Low";
+            case 0x000:
+                return "Normal";
+            case 0x001:
+                return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getEasyShootingModeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_EASY_SHOOTING_MODE,
+            "Full auto",
+            "Manual",
+            "Landscape",
+            "Fast shutter",
+            "Slow shutter",
+            "Night",
+            "B&W",
+            "Sepia",
+            "Portrait",
+            "Sports",
+            "Macro / Closeup",
+            "Pan focus"
+        );
+    }
+
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_IMAGE_SIZE,
+            "Large",
+            "Medium",
+            "Small"
+        );
+    }
+
+    @Nullable
+    public String getFocusMode1Description()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_FOCUS_MODE_1,
+            "One-shot",
+            "AI Servo",
+            "AI Focus",
+            "Manual Focus",
+            // TODO should check field 32 here (FOCUS_MODE_2)
+            "Single",
+            "Continuous",
+            "Manual Focus"
+        );
+    }
+
+    @Nullable
+    public String getContinuousDriveModeDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_CONTINUOUS_DRIVE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                final Integer delay = _directory.getInteger(CameraSettings.TAG_SELF_TIMER_DELAY);
+                if (delay != null)
+                    return delay == 0 ? "Single shot" : "Single shot with self-timer";
+            case 1:
+                return "Continuous";
+        }
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "No flash fired";
+            case 1:
+                return "Auto";
+            case 2:
+                return "On";
+            case 3:
+                return "Red-eye reduction";
+            case 4:
+                return "Slow-synchro";
+            case 5:
+                return "Auto and red-eye reduction";
+            case 6:
+                return "On and red-eye reduction";
+            case 16:
+                // note: this value not set on Canon D30
+                return "External flash";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSelfTimerDelayDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SELF_TIMER_DELAY);
+        if (value == null)
+            return null;
+        if (value == 0) {
+            return "Self timer not used";
+        } else {
+            // TODO find an image that tests this calculation
+            return Double.toString((double)value * 0.1d) + " sec";
+        }
+    }
+
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_MACRO_MODE, 1, "Macro", "Normal");
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_QUALITY, 2, "Normal", "Fine", null, "Superfine");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "No digital zoom", "2x", "4x");
+    }
+
+    @Nullable
+    public String getFocusTypeDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FOCUS_TYPE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Manual";
+            case 1:
+                return "Auto";
+            case 3:
+                return "Close-up (Macro)";
+            case 8:
+                return "Locked (Pan Mode)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashActivityDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FLASH_ACTIVITY, "Flash did not fire", "Flash fired");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,723 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Canon cameras.
+ *
+ * Thanks to Bill Richards for his contribution to this makernote directory.
+ *
+ * Many tag definitions explained here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CanonMakernoteDirectory extends Directory
+{
+    // 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_FLASH_INFO                     = 0x0003;
+    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_CRW_PARAM                       = 0x4002; // 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_BLACK_LEVEL                     = 0x4008; // 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 &amp; 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 Bracketing sequence/auto cancellation
+//     * 0 = 0,-,+ / Enabled
+//     * 1 = 0,-,+ / Disabled
+//     * 2 = -,0,+ / Enabled
+//     * 3 = -,0,+ / Disabled
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_BRACKETING = 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;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _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_BRACKETING, "Auto-Exposure Bracketing 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_CRW_PARAM, "CRW Parameters");
+        _tagNameMap.put(TAG_COLOR_INFO_ARRAY_2, "Color Data Array 2");
+        _tagNameMap.put(TAG_BLACK_LEVEL, "Black Level");
+        _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");
+    }
+
+    public CanonMakernoteDirectory()
+    {
+        this.setDescriptor(new CanonMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Canon Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    @Override
+    public void setObjectArray(int tagType, @NotNull Object array)
+    {
+        // 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: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(CameraSettings.OFFSET + i, ints[i]);
+                break;
+            }
+            case TAG_FOCAL_LENGTH_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(FocalLength.OFFSET + i, ints[i]);
+                break;
+            }
+            case TAG_SHOT_INFO_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(ShotInfo.OFFSET + i, ints[i]);
+                break;
+            }
+            case TAG_PANORAMA_ARRAY: {
+                int[] ints = (int[])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: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(AFInfo.OFFSET + i, ints[i]);
+                break;
+            }
+            default: {
+                // no special handling...
+                super.setObjectArray(tagType, array);
+                break;
+            }
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.CasioType1MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link CasioType1MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory>
+{
+    public CasioType1MakernoteDescriptor(@NotNull CasioType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_RECORDING_MODE:
+                return getRecordingModeDescription();
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_FOCUSING_MODE:
+                return getFocusingModeDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FLASH_INTENSITY:
+                return getFlashIntensityDescription();
+            case TAG_OBJECT_DISTANCE:
+                return getObjectDistanceDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_CCD_SENSITIVITY:
+                return getCcdSensitivityDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getCcdSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CCD_SENSITIVITY);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            // these four for QV3000
+            case 64: return "Normal";
+            case 125: return "+1.0";
+            case 250: return "+2.0";
+            case 244: return "+3.0";
+            // these two for QV8000/2000
+            case 80: return "Normal (ISO 80 equivalent)";
+            case 100: return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal", "Soft", "Hard");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Integer value = _directory.getInteger(TAG_DIGITAL_ZOOM);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0x10000: return "No digital zoom";
+            case 0x10001: return "2x digital zoom";
+            case 0x20000: return "2x digital zoom";
+            case 0x40000: return "4x digital zoom";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 1: return "Auto";
+            case 2: return "Tungsten";
+            case 3: return "Daylight";
+            case 4: return "Florescent";
+            case 5: return "Shade";
+            case 129: return "Manual";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getObjectDistanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE);
+
+        if (value == null)
+            return null;
+
+        return value + " mm";
+    }
+
+    @Nullable
+    public String getFlashIntensityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_FLASH_INTENSITY);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 11: return "Weak";
+            case 13: return "Normal";
+            case 15: return "Strong";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE, 1, "Auto", "On", "Off", "Red eye reduction");
+    }
+
+    @Nullable
+    public String getFocusingModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUSING_MODE, 2, "Macro", "Auto focus", "Manual focus", "Infinity");
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1, "Economy", "Normal", "Fine");
+    }
+
+    @Nullable
+    public String getRecordingModeDescription()
+    {
+        return getIndexedDescription(TAG_RECORDING_MODE, 1, "Single shutter", "Panorama", "Night scene", "Portrait", "Landscape");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * 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 https://drewnoakes.com
+ */
+public class CasioType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_RECORDING_MODE = 0x0001;
+    public static final int TAG_QUALITY = 0x0002;
+    public static final int TAG_FOCUSING_MODE = 0x0003;
+    public static final int TAG_FLASH_MODE = 0x0004;
+    public static final int TAG_FLASH_INTENSITY = 0x0005;
+    public static final int TAG_OBJECT_DISTANCE = 0x0006;
+    public static final int TAG_WHITE_BALANCE = 0x0007;
+    public static final int TAG_UNKNOWN_1 = 0x0008;
+    public static final int TAG_UNKNOWN_2 = 0x0009;
+    public static final int TAG_DIGITAL_ZOOM = 0x000A;
+    public static final int TAG_SHARPNESS = 0x000B;
+    public static final int TAG_CONTRAST = 0x000C;
+    public static final int TAG_SATURATION = 0x000D;
+    public static final int TAG_UNKNOWN_3 = 0x000E;
+    public static final int TAG_UNKNOWN_4 = 0x000F;
+    public static final int TAG_UNKNOWN_5 = 0x0010;
+    public static final int TAG_UNKNOWN_6 = 0x0011;
+    public static final int TAG_UNKNOWN_7 = 0x0012;
+    public static final int TAG_UNKNOWN_8 = 0x0013;
+    public static final int TAG_CCD_SENSITIVITY = 0x0014;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CCD_SENSITIVITY, "CCD Sensitivity");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_FLASH_INTENSITY, "Flash Intensity");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FOCUSING_MODE, "Focusing Mode");
+        _tagNameMap.put(TAG_OBJECT_DISTANCE, "Object Distance");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_RECORDING_MODE, "Recording Mode");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_UNKNOWN_4, "Makernote Unknown 4");
+        _tagNameMap.put(TAG_UNKNOWN_5, "Makernote Unknown 5");
+        _tagNameMap.put(TAG_UNKNOWN_6, "Makernote Unknown 6");
+        _tagNameMap.put(TAG_UNKNOWN_7, "Makernote Unknown 7");
+        _tagNameMap.put(TAG_UNKNOWN_8, "Makernote Unknown 8");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+    }
+
+    public CasioType1MakernoteDirectory()
+    {
+        this.setDescriptor(new CasioType1MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Casio Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.CasioType2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link CasioType2MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory>
+{
+    public CasioType2MakernoteDescriptor(@NotNull CasioType2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_THUMBNAIL_DIMENSIONS:
+                return getThumbnailDimensionsDescription();
+            case TAG_THUMBNAIL_SIZE:
+                return getThumbnailSizeDescription();
+            case TAG_THUMBNAIL_OFFSET:
+                return getThumbnailOffsetDescription();
+            case TAG_QUALITY_MODE:
+                return getQualityModeDescription();
+            case TAG_IMAGE_SIZE:
+                return getImageSizeDescription();
+            case TAG_FOCUS_MODE_1:
+                return getFocusMode1Description();
+            case TAG_ISO_SENSITIVITY:
+                return getIsoSensitivityDescription();
+            case TAG_WHITE_BALANCE_1:
+                return getWhiteBalance1Description();
+            case TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case TAG_PREVIEW_THUMBNAIL:
+                return getCasioPreviewThumbnailDescription();
+            case TAG_WHITE_BALANCE_BIAS:
+                return getWhiteBalanceBiasDescription();
+            case TAG_WHITE_BALANCE_2:
+                return getWhiteBalance2Description();
+            case TAG_OBJECT_DISTANCE:
+                return getObjectDistanceDescription();
+            case TAG_FLASH_DISTANCE:
+                return getFlashDistanceDescription();
+            case TAG_RECORD_MODE:
+                return getRecordModeDescription();
+            case TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_FOCUS_MODE_2:
+                return getFocusMode2Description();
+            case TAG_TIME_ZONE:
+                return getTimeZoneDescription();
+            case TAG_CCD_ISO_SENSITIVITY:
+                return getCcdIsoSensitivityDescription();
+            case TAG_COLOUR_MODE:
+                return getColourModeDescription();
+            case TAG_ENHANCEMENT:
+                return getEnhancementDescription();
+            case TAG_FILTER:
+                return getFilterDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFilterDescription()
+    {
+        return getIndexedDescription(TAG_FILTER, "Off");
+    }
+
+    @Nullable
+    public String getEnhancementDescription()
+    {
+        return getIndexedDescription(TAG_ENHANCEMENT, "Off");
+    }
+
+    @Nullable
+    public String getColourModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOUR_MODE, "Off");
+    }
+
+    @Nullable
+    public String getCcdIsoSensitivityDescription()
+    {
+        return getIndexedDescription(TAG_CCD_ISO_SENSITIVITY, "Off", "On");
+    }
+
+    @Nullable
+    public String getTimeZoneDescription()
+    {
+        return _directory.getString(TAG_TIME_ZONE);
+    }
+
+    @Nullable
+    public String getFocusMode2Description()
+    {
+        Integer value = _directory.getInteger(TAG_FOCUS_MODE_2);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Fixation";
+            case 6: return "Multi-Area Focus";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 3, "Fine");
+    }
+
+    @Nullable
+    public String getSelfTimerDescription()
+    {
+        return getIndexedDescription(TAG_SELF_TIMER, 1, "Off");
+    }
+
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        return getIndexedDescription(TAG_RECORD_MODE, 2, "Normal");
+    }
+
+    @Nullable
+    public String getFlashDistanceDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_DISTANCE, "Off");
+    }
+
+    @Nullable
+    public String getObjectDistanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE);
+        if (value == null)
+            return null;
+        return Integer.toString(value) + " mm";
+    }
+
+    @Nullable
+    public String getWhiteBalance2Description()
+    {
+        Integer value = _directory.getInteger(TAG_WHITE_BALANCE_2);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Manual";
+            case 1: return "Auto"; // unsure about this
+            case 4: return "Flash"; // unsure about this
+            case 12: return "Flash";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceBiasDescription()
+    {
+        return _directory.getString(TAG_WHITE_BALANCE_BIAS);
+    }
+
+    @Nullable
+    public String getCasioPreviewThumbnailDescription()
+    {
+        final byte[] bytes = _directory.getByteArray(TAG_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
+        return _directory.getString(TAG_PRINT_IMAGE_MATCHING_INFO);
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "-1", "Normal", "+1");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "-1", "Normal", "+1");
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION, "-1", "Normal", "+1");
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Double value = _directory.getDoubleObject(TAG_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        return Double.toString(value / 10d) + " mm";
+    }
+
+    @Nullable
+    public String getWhiteBalance1Description()
+    {
+        return getIndexedDescription(
+            TAG_WHITE_BALANCE_1,
+            "Auto",
+            "Daylight",
+            "Shade",
+            "Tungsten",
+            "Florescent",
+            "Manual"
+        );
+    }
+
+    @Nullable
+    public String getIsoSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ISO_SENSITIVITY);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 3: return "50";
+            case 4: return "64";
+            case 6: return "100";
+            case 9: return "200";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFocusMode1Description()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE_1, "Normal", "Macro");
+    }
+
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_IMAGE_SIZE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "640 x 480 pixels";
+            case 4: return "1600 x 1200 pixels";
+            case 5: return "2048 x 1536 pixels";
+            case 20: return "2288 x 1712 pixels";
+            case 21: return "2592 x 1944 pixels";
+            case 22: return "2304 x 1728 pixels";
+            case 36: return "3008 x 2008 pixels";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getQualityModeDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY_MODE, 1, "Fine", "Super Fine");
+    }
+
+    @Nullable
+    public String getThumbnailOffsetDescription()
+    {
+        return _directory.getString(TAG_THUMBNAIL_OFFSET);
+    }
+
+    @Nullable
+    public String getThumbnailSizeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_THUMBNAIL_SIZE);
+        if (value == null)
+            return null;
+        return Integer.toString(value) + " bytes";
+    }
+
+    @Nullable
+    public String getThumbnailDimensionsDescription()
+    {
+        int[] dimensions = _directory.getIntArray(TAG_THUMBNAIL_DIMENSIONS);
+        if (dimensions == null || dimensions.length != 2)
+            return _directory.getString(TAG_THUMBNAIL_DIMENSIONS);
+        return dimensions[0] + " x " + dimensions[1] + " pixels";
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * 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 https://drewnoakes.com
+ */
+public class CasioType2MakernoteDirectory extends Directory
+{
+    /**
+     * 2 values - x,y dimensions in pixels.
+     */
+    public static final int TAG_THUMBNAIL_DIMENSIONS = 0x0002;
+    /**
+     * Size in bytes
+     */
+    public static final int TAG_THUMBNAIL_SIZE = 0x0003;
+    /**
+     * Offset of Preview Thumbnail
+     */
+    public static final int TAG_THUMBNAIL_OFFSET = 0x0004;
+    /**
+     * 1 = Fine
+     * 2 = Super Fine
+     */
+    public static final int TAG_QUALITY_MODE = 0x0008;
+    /**
+     * 0 = 640 x 480 pixels
+     * 4 = 1600 x 1200 pixels
+     * 5 = 2048 x 1536 pixels
+     * 20 = 2288 x 1712 pixels
+     * 21 = 2592 x 1944 pixels
+     * 22 = 2304 x 1728 pixels
+     * 36 = 3008 x 2008 pixels
+     */
+    public static final int TAG_IMAGE_SIZE = 0x0009;
+    /**
+     * 0 = Normal
+     * 1 = Macro
+     */
+    public static final int TAG_FOCUS_MODE_1 = 0x000D;
+    /**
+     * 3 = 50
+     * 4 = 64
+     * 6 = 100
+     * 9 = 200
+     */
+    public static final int TAG_ISO_SENSITIVITY = 0x0014;
+    /**
+     * 0 = Auto
+     * 1 = Daylight
+     * 2 = Shade
+     * 3 = Tungsten
+     * 4 = Fluorescent
+     * 5 = Manual
+     */
+    public static final int TAG_WHITE_BALANCE_1 = 0x0019;
+    /**
+     * Units are tenths of a millimetre
+     */
+    public static final int TAG_FOCAL_LENGTH = 0x001D;
+    /**
+     * 0 = -1
+     * 1 = Normal
+     * 2 = +1
+     */
+    public static final int TAG_SATURATION = 0x001F;
+    /**
+     * 0 = -1
+     * 1 = Normal
+     * 2 = +1
+     */
+    public static final int TAG_CONTRAST = 0x0020;
+    /**
+     * 0 = -1
+     * 1 = Normal
+     * 2 = +1
+     */
+    public static final int TAG_SHARPNESS = 0x0021;
+    /**
+     * See PIM specification here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    /**
+     * Alternate thumbnail offset
+     */
+    public static final int TAG_PREVIEW_THUMBNAIL = 0x2000;
+    /**
+     *
+     */
+    public static final int TAG_WHITE_BALANCE_BIAS = 0x2011;
+    /**
+     * 12 = Flash
+     * 0 = Manual
+     * 1 = Auto?
+     * 4 = Flash?
+     */
+    public static final int TAG_WHITE_BALANCE_2 = 0x2012;
+    /**
+     * Units are millimetres
+     */
+    public static final int TAG_OBJECT_DISTANCE = 0x2022;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_FLASH_DISTANCE = 0x2034;
+    /**
+     * 2 = Normal Mode
+     */
+    public static final int TAG_RECORD_MODE = 0x3000;
+    /**
+     * 1 = Off?
+     */
+    public static final int TAG_SELF_TIMER = 0x3001;
+    /**
+     * 3 = Fine
+     */
+    public static final int TAG_QUALITY = 0x3002;
+    /**
+     * 1 = Fixation
+     * 6 = Multi-Area Auto Focus
+     */
+    public static final int TAG_FOCUS_MODE_2 = 0x3003;
+    /**
+     * (string)
+     */
+    public static final int TAG_TIME_ZONE = 0x3006;
+    /**
+     *
+     */
+    public static final int TAG_BESTSHOT_MODE = 0x3007;
+    /**
+     * 0 = Off
+     * 1 = On?
+     */
+    public static final int TAG_CCD_ISO_SENSITIVITY = 0x3014;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_COLOUR_MODE = 0x3015;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_ENHANCEMENT = 0x3016;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_FILTER = 0x3017;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        // TODO add missing names
+        _tagNameMap.put(TAG_THUMBNAIL_DIMENSIONS, "Thumbnail Dimensions");
+        _tagNameMap.put(TAG_THUMBNAIL_SIZE, "Thumbnail Size");
+        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_QUALITY_MODE, "Quality Mode");
+        _tagNameMap.put(TAG_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(TAG_FOCUS_MODE_1, "Focus Mode");
+        _tagNameMap.put(TAG_ISO_SENSITIVITY, "ISO Sensitivity");
+        _tagNameMap.put(TAG_WHITE_BALANCE_1, "White Balance");
+        _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_PREVIEW_THUMBNAIL, "Casio Preview Thumbnail");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_WHITE_BALANCE_2, "White Balance");
+        _tagNameMap.put(TAG_OBJECT_DISTANCE, "Object Distance");
+        _tagNameMap.put(TAG_FLASH_DISTANCE, "Flash Distance");
+        _tagNameMap.put(TAG_RECORD_MODE, "Record Mode");
+        _tagNameMap.put(TAG_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(TAG_TIME_ZONE, "Time Zone");
+        _tagNameMap.put(TAG_BESTSHOT_MODE, "BestShot Mode");
+        _tagNameMap.put(TAG_CCD_ISO_SENSITIVITY, "CCD ISO Sensitivity");
+        _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_ENHANCEMENT, "Enhancement");
+        _tagNameMap.put(TAG_FILTER, "Filter");
+    }
+
+    public CasioType2MakernoteDirectory()
+    {
+        this.setDescriptor(new CasioType2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Casio Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.FujifilmMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link FujifilmMakernoteDirectory}.
+ * <p>
+ * Fujifilm added their Makernote tag from the Year 2000's models (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.
+ * <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>
+ * There are two big differences to the other manufacturers.
+ * <ul>
+ * <li>Fujifilm's Exif data uses Motorola align, but Makernote ignores it and uses Intel align.</li>
+ * <li>
+ * The other manufacturer's Makernote counts the "offset to data" from the first byte of TIFF header
+ * (same as the other IFD), but Fujifilm counts it from the first byte of Makernote itself.
+ * </li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FujifilmMakernoteDescriptor extends TagDescriptor<FujifilmMakernoteDirectory>
+{
+    public FujifilmMakernoteDescriptor(@NotNull FujifilmMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_COLOR_SATURATION:
+                return getColorSaturationDescription();
+            case TAG_TONE:
+                return getToneDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_NOISE_REDUCTION:
+                return getNoiseReductionDescription();
+            case TAG_HIGH_ISO_NOISE_REDUCTION:
+                return getHighIsoNoiseReductionDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FLASH_EV:
+                return getFlashExposureValueDescription();
+            case TAG_MACRO:
+                return getMacroDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_SLOW_SYNC:
+                return getSlowSyncDescription();
+            case TAG_PICTURE_MODE:
+                return getPictureModeDescription();
+            case TAG_EXR_AUTO:
+                return getExrAutoDescription();
+            case TAG_EXR_MODE:
+                return getExrModeDescription();
+            case TAG_AUTO_BRACKETING:
+                return getAutoBracketingDescription();
+            case TAG_FINE_PIX_COLOR:
+                return getFinePixColorDescription();
+            case TAG_BLUR_WARNING:
+                return getBlurWarningDescription();
+            case TAG_FOCUS_WARNING:
+                return getFocusWarningDescription();
+            case TAG_AUTO_EXPOSURE_WARNING:
+                return getAutoExposureWarningDescription();
+            case TAG_DYNAMIC_RANGE:
+                return getDynamicRangeDescription();
+            case TAG_FILM_MODE:
+                return getFilmModeDescription();
+            case TAG_DYNAMIC_RANGE_SETTING:
+                return getDynamicRangeSettingDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_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";
+            case 0x82: return "Medium Soft";
+            case 0x84: return "Medium Hard";
+            case 0x8000: return "Film Simulation";
+            case 0xFFFF: return "N/A";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Auto";
+            case 0x100: return "Daylight";
+            case 0x200: return "Cloudy";
+            case 0x300: return "Daylight Fluorescent";
+            case 0x301: return "Day White Fluorescent";
+            case 0x302: return "White Fluorescent";
+            case 0x303: return "Warm White Fluorescent";
+            case 0x304: return "Living Room Warm White Fluorescent";
+            case 0x400: return "Incandescence";
+            case 0x500: return "Flash";
+            case 0xf00: return "Custom White Balance";
+            case 0xf01: return "Custom White Balance 2";
+            case 0xf02: return "Custom White Balance 3";
+            case 0xf03: return "Custom White Balance 4";
+            case 0xf04: return "Custom White Balance 5";
+            case 0xff0: return "Kelvin";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorSaturationDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_COLOR_SATURATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x080: return "Medium High";
+            case 0x100: return "High";
+            case 0x180: return "Medium Low";
+            case 0x200: return "Low";
+            case 0x300: return "None (B&W)";
+            case 0x301: return "B&W Green Filter";
+            case 0x302: return "B&W Yellow Filter";
+            case 0x303: return "B&W Blue Filter";
+            case 0x304: return "B&W Sepia";
+            case 0x8000: return "Film Simulation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getToneDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_TONE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x080: return "Medium High";
+            case 0x100: return "High";
+            case 0x180: return "Medium Low";
+            case 0x200: return "Low";
+            case 0x300: return "None (B&W)";
+            case 0x8000: return "Film Simulation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_CONTRAST);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x100: return "High";
+            case 0x300: return "Low";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNoiseReductionDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x040: return "Low";
+            case 0x080: return "Normal";
+            case 0x100: return "N/A";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getHighIsoNoiseReductionDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_HIGH_ISO_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x100: return "Strong";
+            case 0x200: return "Weak";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(
+            TAG_FLASH_MODE,
+            "Auto",
+            "On",
+            "Off",
+            "Red-eye Reduction",
+            "External"
+        );
+    }
+
+    @Nullable
+    public String getFlashExposureValueDescription()
+    {
+        Rational value = _directory.getRational(TAG_FLASH_EV);
+        return value == null ? null : value.toSimpleString(false) + " EV (Apex)";
+    }
+
+    @Nullable
+    public String getMacroDescription()
+    {
+        return getIndexedDescription(TAG_MACRO, "Off", "On");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, "Auto Focus", "Manual Focus");
+    }
+
+    @Nullable
+    public String getSlowSyncDescription()
+    {
+        return getIndexedDescription(TAG_SLOW_SYNC, "Off", "On");
+    }
+
+    @Nullable
+    public String getPictureModeDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_PICTURE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Auto";
+            case 0x001: return "Portrait scene";
+            case 0x002: return "Landscape scene";
+            case 0x003: return "Macro";
+            case 0x004: return "Sports scene";
+            case 0x005: return "Night scene";
+            case 0x006: return "Program AE";
+            case 0x007: return "Natural Light";
+            case 0x008: return "Anti-blur";
+            case 0x009: return "Beach & Snow";
+            case 0x00a: return "Sunset";
+            case 0x00b: return "Museum";
+            case 0x00c: return "Party";
+            case 0x00d: return "Flower";
+            case 0x00e: return "Text";
+            case 0x00f: return "Natural Light & Flash";
+            case 0x010: return "Beach";
+            case 0x011: return "Snow";
+            case 0x012: return "Fireworks";
+            case 0x013: return "Underwater";
+            case 0x014: return "Portrait with Skin Correction";
+            // skip 0x015
+            case 0x016: return "Panorama";
+            case 0x017: return "Night (Tripod)";
+            case 0x018: return "Pro Low-light";
+            case 0x019: return "Pro Focus";
+            // skip 0x01a
+            case 0x01b: return "Dog Face Detection";
+            case 0x01c: return "Cat Face Detection";
+            case 0x100: return "Aperture priority AE";
+            case 0x200: return "Shutter priority AE";
+            case 0x300: return "Manual exposure";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getExrAutoDescription()
+    {
+        return getIndexedDescription(TAG_EXR_AUTO, "Auto", "Manual");
+    }
+
+    @Nullable
+    public String getExrModeDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXR_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x100: return "HR (High Resolution)";
+            case 0x200: return "SN (Signal to Noise Priority)";
+            case 0x300: return "DR (Dynamic Range Priority)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoBracketingDescription()
+    {
+        return getIndexedDescription(
+            TAG_AUTO_BRACKETING,
+            "Off",
+            "On",
+            "No Flash & Flash"
+        );
+    }
+
+    @Nullable
+    public String getFinePixColorDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_FINE_PIX_COLOR);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "Standard";
+            case 0x10: return "Chrome";
+            case 0x30: return "B&W";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getBlurWarningDescription()
+    {
+        return getIndexedDescription(
+            TAG_BLUR_WARNING,
+            "No Blur Warning",
+            "Blur warning"
+        );
+    }
+
+    @Nullable
+    public String getFocusWarningDescription()
+    {
+        return getIndexedDescription(
+            TAG_FOCUS_WARNING,
+            "Good Focus",
+            "Out Of Focus"
+        );
+    }
+
+    @Nullable
+    public String getAutoExposureWarningDescription()
+    {
+        return getIndexedDescription(
+            TAG_AUTO_EXPOSURE_WARNING,
+            "AE Good",
+            "Over Exposed"
+        );
+    }
+
+    @Nullable
+    public String getDynamicRangeDescription()
+    {
+        return getIndexedDescription(
+            TAG_DYNAMIC_RANGE,
+            1,
+            "Standard",
+            null,
+            "Wide"
+        );
+    }
+
+    @Nullable
+    public String getFilmModeDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_FILM_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "F0/Standard (Provia) ";
+            case 0x100: return "F1/Studio Portrait";
+            case 0x110: return "F1a/Studio Portrait Enhanced Saturation";
+            case 0x120: return "F1b/Studio Portrait Smooth Skin Tone (Astia)";
+            case 0x130: return "F1c/Studio Portrait Increased Sharpness";
+            case 0x200: return "F2/Fujichrome (Velvia)";
+            case 0x300: return "F3/Studio Portrait Ex";
+            case 0x400: return "F4/Velvia";
+            case 0x500: return "Pro Neg. Std";
+            case 0x501: return "Pro Neg. Hi";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getDynamicRangeSettingDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_DYNAMIC_RANGE_SETTING);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Auto (100-400%)";
+            case 0x001: return "Manual";
+            case 0x100: return "Standard (100%)";
+            case 0x200: return "Wide 1 (230%)";
+            case 0x201: return "Wide 2 (400%)";
+            case 0x8000: return "Film Simulation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Fujifilm cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FujifilmMakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_VERSION = 0x0000;
+    public static final int TAG_SERIAL_NUMBER = 0x0010;
+
+    public static final int TAG_QUALITY = 0x1000;
+    public static final int TAG_SHARPNESS = 0x1001;
+    public static final int TAG_WHITE_BALANCE = 0x1002;
+    public static final int TAG_COLOR_SATURATION = 0x1003;
+    public static final int TAG_TONE = 0x1004;
+    public static final int TAG_COLOR_TEMPERATURE = 0x1005;
+    public static final int TAG_CONTRAST = 0x1006;
+
+    public static final int TAG_WHITE_BALANCE_FINE_TUNE = 0x100a;
+    public static final int TAG_NOISE_REDUCTION = 0x100b;
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION = 0x100e;
+
+    public static final int TAG_FLASH_MODE = 0x1010;
+    public static final int TAG_FLASH_EV = 0x1011;
+
+    public static final int TAG_MACRO = 0x1020;
+    public static final int TAG_FOCUS_MODE = 0x1021;
+    public static final int TAG_FOCUS_PIXEL = 0x1023;
+
+    public static final int TAG_SLOW_SYNC = 0x1030;
+    public static final int TAG_PICTURE_MODE = 0x1031;
+    public static final int TAG_EXR_AUTO = 0x1033;
+    public static final int TAG_EXR_MODE = 0x1034;
+
+    public static final int TAG_AUTO_BRACKETING = 0x1100;
+    public static final int TAG_SEQUENCE_NUMBER = 0x1101;
+
+    public static final int TAG_FINE_PIX_COLOR = 0x1210;
+
+    public static final int TAG_BLUR_WARNING = 0x1300;
+    public static final int TAG_FOCUS_WARNING = 0x1301;
+    public static final int TAG_AUTO_EXPOSURE_WARNING = 0x1302;
+    public static final int TAG_GE_IMAGE_SIZE = 0x1304;
+
+    public static final int TAG_DYNAMIC_RANGE = 0x1400;
+    public static final int TAG_FILM_MODE = 0x1401;
+    public static final int TAG_DYNAMIC_RANGE_SETTING = 0x1402;
+    public static final int TAG_DEVELOPMENT_DYNAMIC_RANGE = 0x1403;
+    public static final int TAG_MIN_FOCAL_LENGTH = 0x1404;
+    public static final int TAG_MAX_FOCAL_LENGTH = 0x1405;
+    public static final int TAG_MAX_APERTURE_AT_MIN_FOCAL = 0x1406;
+    public static final int TAG_MAX_APERTURE_AT_MAX_FOCAL = 0x1407;
+
+    public static final int TAG_AUTO_DYNAMIC_RANGE = 0x140b;
+
+    public static final int TAG_FACES_DETECTED = 0x4100;
+    /**
+     * Left, top, right and bottom coordinates in full-sized image for each face detected.
+     */
+    public static final int TAG_FACE_POSITIONS = 0x4103;
+    public static final int TAG_FACE_REC_INFO = 0x4282;
+
+    public static final int TAG_FILE_SOURCE = 0x8000;
+    public static final int TAG_ORDER_NUMBER = 0x8002;
+    public static final int TAG_FRAME_NUMBER = 0x8003;
+
+    public static final int TAG_PARALLAX = 0xb211;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_COLOR_SATURATION, "Color Saturation");
+        _tagNameMap.put(TAG_TONE, "Tone (Contrast)");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+
+        _tagNameMap.put(TAG_WHITE_BALANCE_FINE_TUNE, "White Balance Fine Tune");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FLASH_EV, "Flash Strength");
+
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FOCUS_PIXEL, "Focus Pixel");
+
+        _tagNameMap.put(TAG_SLOW_SYNC, "Slow Sync");
+        _tagNameMap.put(TAG_PICTURE_MODE, "Picture Mode");
+        _tagNameMap.put(TAG_EXR_AUTO, "EXR Auto");
+        _tagNameMap.put(TAG_EXR_MODE, "EXR Mode");
+
+        _tagNameMap.put(TAG_AUTO_BRACKETING, "Auto Bracketing");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+
+        _tagNameMap.put(TAG_FINE_PIX_COLOR, "FinePix Color Setting");
+
+        _tagNameMap.put(TAG_BLUR_WARNING, "Blur Warning");
+        _tagNameMap.put(TAG_FOCUS_WARNING, "Focus Warning");
+        _tagNameMap.put(TAG_AUTO_EXPOSURE_WARNING, "AE Warning");
+        _tagNameMap.put(TAG_GE_IMAGE_SIZE, "GE Image Size");
+
+        _tagNameMap.put(TAG_DYNAMIC_RANGE, "Dynamic Range");
+        _tagNameMap.put(TAG_FILM_MODE, "Film Mode");
+        _tagNameMap.put(TAG_DYNAMIC_RANGE_SETTING, "Dynamic Range Setting");
+        _tagNameMap.put(TAG_DEVELOPMENT_DYNAMIC_RANGE, "Development Dynamic Range");
+        _tagNameMap.put(TAG_MIN_FOCAL_LENGTH, "Minimum Focal Length");
+        _tagNameMap.put(TAG_MAX_FOCAL_LENGTH, "Maximum Focal Length");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MIN_FOCAL, "Maximum Aperture at Minimum Focal Length");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MAX_FOCAL, "Maximum Aperture at Maximum Focal Length");
+
+        _tagNameMap.put(TAG_AUTO_DYNAMIC_RANGE, "Auto Dynamic Range");
+
+        _tagNameMap.put(TAG_FACES_DETECTED, "Faces Detected");
+        _tagNameMap.put(TAG_FACE_POSITIONS, "Face Positions");
+        _tagNameMap.put(TAG_FACE_REC_INFO, "Face Detection Data");
+
+        _tagNameMap.put(TAG_FILE_SOURCE, "File Source");
+        _tagNameMap.put(TAG_ORDER_NUMBER, "Order Number");
+        _tagNameMap.put(TAG_FRAME_NUMBER, "Frame Number");
+
+        _tagNameMap.put(TAG_PARALLAX, "Parallax");
+    }
+
+    public FujifilmMakernoteDirectory()
+    {
+        this.setDescriptor(new FujifilmMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Fujifilm Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.KodakMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link KodakMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory>
+{
+    public KodakMakernoteDescriptor(@NotNull KodakMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_BURST_MODE:
+                return getBurstModeDescription();
+            case TAG_SHUTTER_MODE:
+                return getShutterModeDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FLASH_FIRED:
+                return getFlashFiredDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal");
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_COLOR_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x001: case 0x2000: return "B&W";
+            case 0x002: case 0x4000: return "Sepia";
+            case 0x003: return "B&W Yellow Filter";
+            case 0x004: return "B&W Red Filter";
+            case 0x020: return "Saturated Color";
+            case 0x040: case 0x200: return "Neutral Color";
+            case 0x100: return "Saturated Color";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashFiredDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_FIRED, "No", "Yes");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_FLASH_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "Auto";
+            case 0x10: case 0x01: return "Fill Flash";
+            case 0x20: case 0x02: return "Off";
+            case 0x40: case 0x03: return "Red Eye";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE, "Auto", "Flash", "Tungsten", "Daylight");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, "Normal", null, "Macro");
+    }
+
+    @Nullable
+    public String getShutterModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SHUTTER_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Auto";
+            case 8: return "Aperture Priority";
+            case 32: return "Manual";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getBurstModeDescription()
+    {
+        return getIndexedDescription(TAG_BURST_MODE, "Off", "On");
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1, "Fine", "Normal");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Kodak cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KodakMakernoteDirectory extends Directory
+{
+    public final static int TAG_KODAK_MODEL = 0;
+    public final static int TAG_QUALITY = 9;
+    public final static int TAG_BURST_MODE = 10;
+    public final static int TAG_IMAGE_WIDTH = 12;
+    public final static int TAG_IMAGE_HEIGHT = 14;
+    public final static int TAG_YEAR_CREATED = 16;
+    public final static int TAG_MONTH_DAY_CREATED = 18;
+    public final static int TAG_TIME_CREATED = 20;
+    public final static int TAG_BURST_MODE_2 = 24;
+    public final static int TAG_SHUTTER_MODE = 27;
+    public final static int TAG_METERING_MODE = 28;
+    public final static int TAG_SEQUENCE_NUMBER = 29;
+    public final static int TAG_F_NUMBER = 30;
+    public final static int TAG_EXPOSURE_TIME = 32;
+    public final static int TAG_EXPOSURE_COMPENSATION = 36;
+    public final static int TAG_FOCUS_MODE = 56;
+    public final static int TAG_WHITE_BALANCE = 64;
+    public final static int TAG_FLASH_MODE = 92;
+    public final static int TAG_FLASH_FIRED = 93;
+    public final static int TAG_ISO_SETTING = 94;
+    public final static int TAG_ISO = 96;
+    public final static int TAG_TOTAL_ZOOM = 98;
+    public final static int TAG_DATE_TIME_STAMP = 100;
+    public final static int TAG_COLOR_MODE = 102;
+    public final static int TAG_DIGITAL_ZOOM = 104;
+    public final static int TAG_SHARPNESS = 107;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_KODAK_MODEL, "Kodak Model");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_BURST_MODE, "Burst Mode");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_YEAR_CREATED, "Year Created");
+        _tagNameMap.put(TAG_MONTH_DAY_CREATED, "Month/Day Created");
+        _tagNameMap.put(TAG_TIME_CREATED, "Time Created");
+        _tagNameMap.put(TAG_BURST_MODE_2, "Burst Mode 2");
+        _tagNameMap.put(TAG_SHUTTER_MODE, "Shutter Speed");
+        _tagNameMap.put(TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(TAG_F_NUMBER, "F Number");
+        _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time");
+        _tagNameMap.put(TAG_EXPOSURE_COMPENSATION, "Exposure Compensation");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FLASH_FIRED, "Flash Fired");
+        _tagNameMap.put(TAG_ISO_SETTING, "ISO Setting");
+        _tagNameMap.put(TAG_ISO, "ISO");
+        _tagNameMap.put(TAG_TOTAL_ZOOM, "Total Zoom");
+        _tagNameMap.put(TAG_DATE_TIME_STAMP, "Date/Time Stamp");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+    }
+
+    public KodakMakernoteDirectory()
+    {
+        this.setDescriptor(new KodakMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Kodak Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.KyoceraMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link KyoceraMakernoteDirectory}.
+ * <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 https://drewnoakes.com
+ */
+public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDirectory>
+{
+    public KyoceraMakernoteDescriptor(@NotNull KyoceraMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case TAG_PROPRIETARY_THUMBNAIL:
+                return getProprietaryThumbnailDataDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
+    {
+        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
+    }
+
+    @Nullable
+    public String getProprietaryThumbnailDataDescription()
+    {
+        return getByteLengthDescription(TAG_PROPRIETARY_THUMBNAIL);
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Kyocera and Contax cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KyoceraMakernoteDirectory extends Directory
+{
+    public static final int TAG_PROPRIETARY_THUMBNAIL = 0x0001;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_PROPRIETARY_THUMBNAIL, "Proprietary Thumbnail Format Data");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+    }
+
+    public KyoceraMakernoteDirectory()
+    {
+        this.setDescriptor(new KyoceraMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Kyocera/Contax Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.LeicaMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link LeicaMakernoteDirectory}.
+ * <p>
+ * Tag reference from: http://gvsoft.homedns.org/exif/makernote-leica-type1.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class LeicaMakernoteDescriptor extends TagDescriptor<LeicaMakernoteDirectory>
+{
+    public LeicaMakernoteDescriptor(@NotNull LeicaMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_USER_PROFILE:
+                return getUserProfileDescription();
+//            case TAG_SERIAL:
+//                return getSerialNumberDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE:
+                return getExternalSensorBrightnessValueDescription();
+            case TAG_MEASURED_LV:
+                return getMeasuredLvDescription();
+            case TAG_APPROXIMATE_F_NUMBER:
+                return getApproximateFNumberDescription();
+            case TAG_CAMERA_TEMPERATURE:
+                return getCameraTemperatureDescription();
+            case TAG_WB_RED_LEVEL:
+            case TAG_WB_BLUE_LEVEL:
+            case TAG_WB_GREEN_LEVEL:
+                return getSimpleRational(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getCameraTemperatureDescription()
+    {
+        return getFormattedInt(TAG_CAMERA_TEMPERATURE, "%d C");
+    }
+
+    @Nullable
+    private String getApproximateFNumberDescription()
+    {
+        return getSimpleRational(TAG_APPROXIMATE_F_NUMBER);
+    }
+
+    @Nullable
+    private String getMeasuredLvDescription()
+    {
+        return getSimpleRational(TAG_MEASURED_LV);
+    }
+
+    @Nullable
+    private String getExternalSensorBrightnessValueDescription()
+    {
+        return getSimpleRational(TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE);
+    }
+
+    @Nullable
+    private String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            "Auto or Manual",
+            "Daylight",
+            "Fluorescent",
+            "Tungsten",
+            "Flash",
+            "Cloudy",
+            "Shadow"
+        );
+    }
+
+    @Nullable
+    private String getUserProfileDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1,
+            "User Profile 1",
+            "User Profile 2",
+            "User Profile 3",
+            "User Profile 0 (Dynamic)"
+        );
+    }
+
+    @Nullable
+    private String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1, "Fine", "Basic");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to certain Leica cameras.
+ * <p>
+ * Tag reference from: http://gvsoft.homedns.org/exif/makernote-leica-type1.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class LeicaMakernoteDirectory extends Directory
+{
+    public static final int TAG_QUALITY = 0x0300;
+    public static final int TAG_USER_PROFILE = 0x0302;
+    public static final int TAG_SERIAL_NUMBER = 0x0303;
+    public static final int TAG_WHITE_BALANCE = 0x0304;
+
+    public static final int TAG_LENS_TYPE = 0x0310;
+    public static final int TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE = 0x0311;
+    public static final int TAG_MEASURED_LV = 0x0312;
+    public static final int TAG_APPROXIMATE_F_NUMBER = 0x0313;
+
+    public static final int TAG_CAMERA_TEMPERATURE = 0x0320;
+    public static final int TAG_COLOR_TEMPERATURE = 0x0321;
+    public static final int TAG_WB_RED_LEVEL = 0x0322;
+    public static final int TAG_WB_GREEN_LEVEL = 0x0323;
+    public static final int TAG_WB_BLUE_LEVEL = 0x0324;
+
+    public static final int TAG_CCD_VERSION = 0x0330;
+    public static final int TAG_CCD_BOARD_VERSION = 0x0331;
+    public static final int TAG_CONTROLLER_BOARD_VERSION = 0x0332;
+    public static final int TAG_M16_C_VERSION = 0x0333;
+
+    public static final int TAG_IMAGE_ID_NUMBER = 0x0340;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_USER_PROFILE, "User Profile");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE, "External Sensor Brightness Value");
+        _tagNameMap.put(TAG_MEASURED_LV, "Measured LV");
+        _tagNameMap.put(TAG_APPROXIMATE_F_NUMBER, "Approximate F Number");
+
+        _tagNameMap.put(TAG_CAMERA_TEMPERATURE, "Camera Temperature");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_WB_RED_LEVEL, "WB Red Level");
+        _tagNameMap.put(TAG_WB_GREEN_LEVEL, "WB Green Level");
+        _tagNameMap.put(TAG_WB_BLUE_LEVEL, "WB Blue Level");
+
+        _tagNameMap.put(TAG_CCD_VERSION, "CCD Version");
+        _tagNameMap.put(TAG_CCD_BOARD_VERSION, "CCD Board Version");
+        _tagNameMap.put(TAG_CONTROLLER_BOARD_VERSION, "Controller Board Version");
+        _tagNameMap.put(TAG_M16_C_VERSION, "M16 C Version");
+
+        _tagNameMap.put(TAG_IMAGE_ID_NUMBER, "Image ID Number");
+    }
+
+    public LeicaMakernoteDirectory()
+    {
+        this.setDescriptor(new LeicaMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Leica Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.NikonType1MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link NikonType1MakernoteDirectory}.
+ * <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
+ * structure is shown below.
+ * <pre><code>
+ * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
+ * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
+ * </code></pre>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType1MakernoteDescriptor extends TagDescriptor<NikonType1MakernoteDirectory>
+{
+    public NikonType1MakernoteDescriptor(@NotNull NikonType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_IMAGE_ADJUSTMENT:
+                return getImageAdjustmentDescription();
+            case TAG_CCD_SENSITIVITY:
+                return getCcdSensitivityDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FOCUS:
+                return getFocusDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_CONVERTER:
+                return getConverterDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getConverterDescription()
+    {
+        return getIndexedDescription(TAG_CONVERTER, "None", "Fisheye converter");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+                ? "No digital zoom"
+                : value.toSimpleString(true) + "x digital zoom";
+    }
+
+    @Nullable
+    public String getFocusDescription()
+    {
+        Rational value = _directory.getRational(TAG_FOCUS);
+        return value == null
+            ? null
+            : value.getNumerator() == 1 && value.getDenominator() == 0
+                ? "Infinite"
+                : value.toSimpleString(true);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            "Auto",
+            "Preset",
+            "Daylight",
+            "Incandescence",
+            "Florescence",
+            "Cloudy",
+            "SpeedLight"
+        );
+    }
+
+    @Nullable
+    public String getCcdSensitivityDescription()
+    {
+        return getIndexedDescription(TAG_CCD_SENSITIVITY,
+            "ISO80",
+            null,
+            "ISO160",
+            null,
+            "ISO320",
+            "ISO100"
+        );
+    }
+
+    @Nullable
+    public String getImageAdjustmentDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_ADJUSTMENT,
+            "Normal",
+            "Bright +",
+            "Bright -",
+            "Contrast +",
+            "Contrast -"
+        );
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_MODE,
+            1,
+            "Color",
+            "Monochrome"
+        );
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY,
+            1,
+            "VGA Basic",
+            "VGA Normal",
+            "VGA Fine",
+            "SXGA Basic",
+            "SXGA Normal",
+            "SXGA Fine"
+        );
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * 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
+ * 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
+ * structure is shown below.
+ * <pre><code>
+ * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
+ * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
+ * </code></pre>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_UNKNOWN_1 = 0x0002;
+    public static final int TAG_QUALITY = 0x0003;
+    public static final int TAG_COLOR_MODE = 0x0004;
+    public static final int TAG_IMAGE_ADJUSTMENT = 0x0005;
+    public static final int TAG_CCD_SENSITIVITY = 0x0006;
+    public static final int TAG_WHITE_BALANCE = 0x0007;
+    public static final int TAG_FOCUS = 0x0008;
+    public static final int TAG_UNKNOWN_2 = 0x0009;
+    public static final int TAG_DIGITAL_ZOOM = 0x000A;
+    public static final int TAG_CONVERTER = 0x000B;
+    public static final int TAG_UNKNOWN_3 = 0x0F00;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CCD_SENSITIVITY, "CCD Sensitivity");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_CONVERTER, "Fisheye Converter");
+        _tagNameMap.put(TAG_FOCUS, "Focus");
+        _tagNameMap.put(TAG_IMAGE_ADJUSTMENT, "Image Adjustment");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+    }
+
+    public NikonType1MakernoteDirectory()
+    {
+        this.setDescriptor(new NikonType1MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Nikon Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,359 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DecimalFormat;
+
+import static com.drew.metadata.exif.makernotes.NikonType2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link NikonType2MakernoteDirectory}.
+ *
+ * Type-2 applies to the E990 and D-series cameras such as the D1, D70 and D100.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2MakernoteDirectory>
+{
+    public NikonType2MakernoteDescriptor(@NotNull NikonType2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType)
+        {
+            case TAG_PROGRAM_SHIFT:
+                return getProgramShiftDescription();
+            case TAG_EXPOSURE_DIFFERENCE:
+                return getExposureDifferenceDescription();
+            case TAG_LENS:
+                return getLensDescription();
+            case TAG_CAMERA_HUE_ADJUSTMENT:
+                return getHueAdjustmentDescription();
+            case TAG_CAMERA_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_AUTO_FLASH_COMPENSATION:
+                return getAutoFlashCompensationDescription();
+            case TAG_FLASH_EXPOSURE_COMPENSATION:
+                return getFlashExposureCompensationDescription();
+            case TAG_FLASH_BRACKET_COMPENSATION:
+                return getFlashBracketCompensationDescription();
+            case TAG_EXPOSURE_TUNING:
+                return getExposureTuningDescription();
+            case TAG_LENS_STOPS:
+                return getLensStopsDescription();
+            case TAG_COLOR_SPACE:
+                return getColorSpaceDescription();
+            case TAG_ACTIVE_D_LIGHTING:
+                return getActiveDLightingDescription();
+            case TAG_VIGNETTE_CONTROL:
+                return getVignetteControlDescription();
+            case TAG_ISO_1:
+                return getIsoSettingDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_FLASH_USED:
+                return getFlashUsedDescription();
+            case TAG_AF_FOCUS_POSITION:
+                return getAutoFocusPositionDescription();
+            case TAG_FIRMWARE_VERSION:
+                return getFirmwareVersionDescription();
+            case TAG_LENS_TYPE:
+                return getLensTypeDescription();
+            case TAG_SHOOTING_MODE:
+                return getShootingModeDescription();
+            case TAG_NEF_COMPRESSION:
+                return getNEFCompressionDescription();
+            case TAG_HIGH_ISO_NOISE_REDUCTION:
+                return getHighISONoiseReductionDescription();
+            case TAG_POWER_UP_TIME:
+                return getPowerUpTimeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPowerUpTimeDescription()
+    {
+        return getEpochTimeDescription(TAG_POWER_UP_TIME);
+    }
+
+    @Nullable
+    public String getHighISONoiseReductionDescription()
+    {
+        return getIndexedDescription(TAG_HIGH_ISO_NOISE_REDUCTION,
+            "Off",
+            "Minimal",
+            "Low",
+            null,
+            "Normal",
+            null,
+            "High"
+        );
+    }
+
+    @Nullable
+    public String getFlashUsedDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_USED,
+            "Flash Not Used",
+            "Manual Flash",
+            null,
+            "Flash Not Ready",
+            null,
+            null,
+            null,
+            "External Flash",
+            "Fired, Commander Mode",
+            "Fired, TTL Mode"
+        );
+    }
+
+    @Nullable
+    public String getNEFCompressionDescription()
+    {
+        return getIndexedDescription(TAG_NEF_COMPRESSION,
+            1,
+            "Lossy (Type 1)",
+            null,
+            "Uncompressed",
+            null,
+            null,
+            null,
+            "Lossless",
+            "Lossy (Type 2)"
+        );
+    }
+
+    @Nullable
+    public String getShootingModeDescription()
+    {
+        return getBitFlagDescription(TAG_SHOOTING_MODE,
+            // LSB [low label, high label]
+            new String[]{"Single Frame", "Continuous"},
+            "Delay",
+            null,
+            "PC Control",
+            "Exposure Bracketing",
+            "Auto ISO",
+            "White-Balance Bracketing",
+            "IR Control"
+        );
+    }
+
+    @Nullable
+    public String getLensTypeDescription()
+    {
+        return getBitFlagDescription(TAG_LENS_TYPE,
+            // LSB [low label, high label]
+            new String[]{"AF", "MF"},
+            "D",
+            "G",
+            "VR"
+        );
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_SPACE,
+            1,
+            "sRGB",
+            "Adobe RGB"
+        );
+    }
+
+    @Nullable
+    public String getActiveDLightingDescription()
+    {
+        Integer value = _directory.getInteger(TAG_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(TAG_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(TAG_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(TAG_AF_FOCUS_POSITION) + ")";
+        }
+        switch (values[1]) {
+            case 0:
+                return "Centre";
+            case 1:
+                return "Top";
+            case 2:
+                return "Bottom";
+            case 3:
+                return "Left";
+            case 4:
+                return "Right";
+            default:
+                return "Unknown (" + values[1] + ")";
+        }
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM);
+        if (value==null)
+            return null;
+        return value.intValue() == 1
+                ? "No digital zoom"
+                : value.toSimpleString(true) + "x digital zoom";
+    }
+
+    @Nullable
+    public String getProgramShiftDescription()
+    {
+        return getEVDescription(TAG_PROGRAM_SHIFT);
+    }
+
+    @Nullable
+    public String getExposureDifferenceDescription()
+    {
+        return getEVDescription(TAG_EXPOSURE_DIFFERENCE);
+    }
+
+    @Nullable
+    public String getAutoFlashCompensationDescription()
+    {
+        return getEVDescription(TAG_AUTO_FLASH_COMPENSATION);
+    }
+
+    @Nullable
+    public String getFlashExposureCompensationDescription()
+    {
+        return getEVDescription(TAG_FLASH_EXPOSURE_COMPENSATION);
+    }
+
+    @Nullable
+    public String getFlashBracketCompensationDescription()
+    {
+        return getEVDescription(TAG_FLASH_BRACKET_COMPENSATION);
+    }
+
+    @Nullable
+    public String getExposureTuningDescription()
+    {
+        return getEVDescription(TAG_EXPOSURE_TUNING);
+    }
+
+    @Nullable
+    public String getLensStopsDescription()
+    {
+        return getEVDescription(TAG_LENS_STOPS);
+    }
+
+    @Nullable
+    private String getEVDescription(int tagType)
+    {
+        int[] values = _directory.getIntArray(tagType);
+        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(TAG_ISO_1);
+        if (values == null)
+            return null;
+        if (values[0] != 0 || values[1] == 0)
+            return "Unknown (" + _directory.getString(TAG_ISO_1) + ")";
+        return "ISO " + values[1];
+    }
+
+    @Nullable
+    public String getLensDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TAG_LENS);
+
+        return values == null
+            ? null
+            : values.length < 4
+                ? _directory.getString(TAG_LENS)
+                : String.format("%d-%dmm f/%.1f-%.1f", values[0].intValue(), values[1].intValue(), values[2].floatValue(), values[3].floatValue());
+
+    }
+
+    @Nullable
+    public String getHueAdjustmentDescription()
+    {
+        return getFormattedString(TAG_CAMERA_HUE_ADJUSTMENT, "%s degrees");
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        String value = _directory.getString(TAG_CAMERA_COLOR_MODE);
+        return value == null ? null : value.startsWith("MODE1") ? "Mode I (sRGB)" : value;
+    }
+
+    @Nullable
+    public String getFirmwareVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FIRMWARE_VERSION, 2);
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,924 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * 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 https://drewnoakes.com
+ */
+public class NikonType2MakernoteDirectory extends Directory
+{
+    /**
+     * Values observed
+     * - 0200 (D70)
+     * - 0200 (D1X)
+     */
+    public static final int TAG_FIRMWARE_VERSION = 0x0001;
+
+    /**
+     * Values observed
+     * - 0 250
+     * - 0 400
+     */
+    public static final int TAG_ISO_1 = 0x0002;
+
+    /**
+     * The camera's color mode, as an uppercase string.  Examples include:
+     * <ul>
+     * <li><code>B &amp; 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_COLOR_MODE = 0x0003;
+
+    /**
+     * 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_QUALITY_AND_FILE_FORMAT = 0x0004;
+
+    /**
+     * 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_CAMERA_WHITE_BALANCE  = 0x0005;
+
+    /**
+     * 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_CAMERA_SHARPENING = 0x0006;
+
+    /**
+     * 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_AF_TYPE = 0x0007;
+
+    /**
+     * 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_AUTO_FLASH_MODE is blank (whitespace), Nikon Browser displays "Flash Sync Mode: Not Attached"
+     */
+    public static final int TAG_FLASH_SYNC_MODE = 0x0008;
+
+    /**
+     * 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_AUTO_FLASH_MODE = 0x0009;
+
+    /**
+     * 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_UNKNOWN_34 = 0x000A;
+
+    /**
+     * 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_CAMERA_WHITE_BALANCE_FINE = 0x000B;
+
+    /**
+     * The first two numbers are coefficients to multiply red and blue channels according to white balance as set in the
+     * camera. The meaning of the third and the fourth numbers is unknown.
+     *
+     * Values observed
+     * - 2.25882352 1.76078431 0.0 0.0
+     * - 10242/1 34305/1 0/1 0/1
+     * - 234765625/100000000 1140625/1000000 1/1 1/1
+     */
+    public static final int TAG_CAMERA_WHITE_BALANCE_RB_COEFF = 0x000C;
+
+    /**
+     * 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_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_EXPOSURE_DIFFERENCE = 0x000E;
+
+    /**
+     * The camera's ISO mode, as an uppercase string.
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>MANUAL</code></li>
+     * </ul>
+     */
+    public static final int TAG_ISO_MODE = 0x000F;
+
+    /**
+     * Added during merge of Type2 &amp; Type3.  May apply to earlier models, such as E990 and D1.
+     */
+    public static final int TAG_DATA_DUMP = 0x0010;
+
+    /**
+     * 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_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_AUTO_FLASH_COMPENSATION = 0x0012;
+
+    /**
+     * 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_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)
+     * <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_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_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_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_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_FLASH_MODE = 0x001a;
+
+    public static final int TAG_CROP_HIGH_SPEED = 0x001b;
+    public static final int TAG_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_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_COLOR_SPACE = 0x001e;
+    public static final int TAG_VR_INFO = 0x001f;
+    public static final int TAG_IMAGE_AUTHENTICATION = 0x0020;
+    public static final int TAG_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_ACTIVE_D_LIGHTING = 0x0022;
+    public static final int TAG_PICTURE_CONTROL = 0x0023;
+    public static final int TAG_WORLD_TIME = 0x0024;
+    public static final int TAG_ISO_INFO = 0x0025;
+    public static final int TAG_UNKNOWN_36 = 0x0026;
+    public static final int TAG_UNKNOWN_37 = 0x0027;
+    public static final int TAG_UNKNOWN_38 = 0x0028;
+    public static final int TAG_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_VIGNETTE_CONTROL = 0x002a;
+    public static final int TAG_UNKNOWN_40 = 0x002b;
+    public static final int TAG_UNKNOWN_41 = 0x002c;
+    public static final int TAG_UNKNOWN_42 = 0x002d;
+    public static final int TAG_UNKNOWN_43 = 0x002e;
+    public static final int TAG_UNKNOWN_44 = 0x002f;
+    public static final int TAG_UNKNOWN_45 = 0x0030;
+    public static final int TAG_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 &amp; W</code></li>
+     * <li><code>BRIGHTNESS(+)</code></li>
+     * <li><code>BRIGHTNESS(-)</code></li>
+     * <li><code>SEPIA</code></li>
+     * </ul>
+     */
+    public static final int TAG_IMAGE_ADJUSTMENT = 0x0080;
+
+    /**
+     * 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_CAMERA_TONE_COMPENSATION = 0x0081;
+
+    /**
+     * 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_ADAPTER = 0x0082;
+
+    /**
+     * 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_LENS_TYPE = 0x0083;
+
+    /**
+     * A pair of focal/max-fstop values that describe the lens used.
+     *
+     * Values observed
+     * - 180.0,180.0,2.8,2.8 (D100)
+     * - 240/10 850/10 35/10 45/10
+     * - 18-70mm f/3.5-4.5 (D70)
+     * - 17-35mm f/2.8-2.8 (D1X)
+     * - 70-200mm f/2.8-2.8 (D70)
+     *
+     * Nikon Browser identifies the lens as "18-70mm F/3.5-4.5 G" which
+     * is identical to metadata extractor, except for the "G".  This must
+     * be coming from another tag...
+     */
+    public static final int TAG_LENS = 0x0084;
+
+    /**
+     * Added during merge of Type2 &amp; Type3.  May apply to earlier models, such as E990 and D1.
+     */
+    public static final int TAG_MANUAL_FOCUS_DISTANCE = 0x0085;
+
+    /**
+     * The amount of digital zoom used.
+     */
+    public static final int TAG_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_FLASH_USED = 0x0087;
+
+    /**
+     * The position of the autofocus target.
+     */
+    public static final int TAG_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_SHOOTING_MODE = 0x0089;
+
+    public static final int TAG_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_LENS_STOPS = 0x008B;
+
+    public static final int TAG_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 &amp; W</code> = B &amp; W</li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_COLOR_MODE = 0x008D;
+    public static final int TAG_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_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_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_SHOT_INFO = 0x0091;
+
+    /**
+     * The hue adjustment as set in the camera.  Values observed are either 0 or 3.
+     */
+    public static final int TAG_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_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&amp;W)</li>
+     * </ul>
+     */
+    public static final int TAG_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_NOISE_REDUCTION = 0x0095;
+    public static final int TAG_LINEARIZATION_TABLE = 0x0096;
+    public static final int TAG_COLOR_BALANCE = 0x0097;
+    public static final int TAG_LENS_DATA = 0x0098;
+
+    /** The NEF (RAW) thumbnail size, as an integer array with two items representing [width,height]. */
+    public static final int TAG_NEF_THUMBNAIL_SIZE = 0x0099;
+
+    /** The sensor pixel size, as a pair of rational numbers. */
+    public static final int TAG_SENSOR_PIXEL_SIZE = 0x009A;
+    public static final int TAG_UNKNOWN_10 = 0x009B;
+    public static final int TAG_SCENE_ASSIST = 0x009C;
+    public static final int TAG_UNKNOWN_11 = 0x009D;
+    public static final int TAG_RETOUCH_HISTORY = 0x009E;
+    public static final int TAG_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_CAMERA_SERIAL_NUMBER_2 = 0x00A0;
+
+    public static final int TAG_IMAGE_DATA_SIZE = 0x00A2;
+
+    public static final int TAG_UNKNOWN_27 = 0x00A3;
+    public static final int TAG_UNKNOWN_28 = 0x00A4;
+    public static final int TAG_IMAGE_COUNT = 0x00A5;
+    public static final int TAG_DELETED_IMAGE_COUNT = 0x00A6;
+
+    /** The number of total shutter releases.  This value increments for each exposure (observed on D70). */
+    public static final int TAG_EXPOSURE_SEQUENCE_NUMBER = 0x00A7;
+
+    public static final int TAG_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_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_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_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_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_AF_RESPONSE = 0x00AD;
+    public static final int TAG_UNKNOWN_29 = 0x00AE;
+    public static final int TAG_UNKNOWN_30 = 0x00AF;
+    public static final int TAG_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_HIGH_ISO_NOISE_REDUCTION = 0x00B1;
+    public static final int TAG_UNKNOWN_31 = 0x00B2;
+    public static final int TAG_UNKNOWN_32 = 0x00B3;
+    public static final int TAG_UNKNOWN_33 = 0x00B4;
+    public static final int TAG_UNKNOWN_48 = 0x00B5;
+    public static final int TAG_POWER_UP_TIME = 0x00B6;
+    public static final int TAG_AF_INFO_2 = 0x00B7;
+    public static final int TAG_FILE_INFO = 0x00B8;
+    public static final int TAG_AF_TUNE = 0x00B9;
+    public static final int TAG_UNKNOWN_49 = 0x00BB;
+    public static final int TAG_UNKNOWN_50 = 0x00BD;
+    public static final int TAG_UNKNOWN_51 = 0x0103;
+    public static final int TAG_PRINT_IM = 0x0E00;
+
+    /**
+     * Data about changes set by Nikon Capture Editor.
+     *
+     * Values observed
+     */
+    public static final int TAG_NIKON_CAPTURE_DATA = 0x0E01;
+    public static final int TAG_UNKNOWN_52 = 0x0E05;
+    public static final int TAG_UNKNOWN_53 = 0x0E08;
+    public static final int TAG_NIKON_CAPTURE_VERSION = 0x0E09;
+    public static final int TAG_NIKON_CAPTURE_OFFSETS = 0x0E0E;
+    public static final int TAG_NIKON_SCAN = 0x0E10;
+    public static final int TAG_UNKNOWN_54 = 0x0E19;
+    public static final int TAG_NEF_BIT_DEPTH = 0x0E22;
+    public static final int TAG_UNKNOWN_55 = 0x0E23;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_ISO_1, "ISO");
+        _tagNameMap.put(TAG_QUALITY_AND_FILE_FORMAT, "Quality & File Format");
+        _tagNameMap.put(TAG_CAMERA_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_CAMERA_SHARPENING, "Sharpening");
+        _tagNameMap.put(TAG_AF_TYPE, "AF Type");
+        _tagNameMap.put(TAG_CAMERA_WHITE_BALANCE_FINE, "White Balance Fine");
+        _tagNameMap.put(TAG_CAMERA_WHITE_BALANCE_RB_COEFF, "White Balance RB Coefficients");
+        _tagNameMap.put(TAG_ISO_REQUESTED, "ISO");
+        _tagNameMap.put(TAG_ISO_MODE, "ISO Mode");
+        _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
+
+        _tagNameMap.put(TAG_PROGRAM_SHIFT, "Program Shift");
+        _tagNameMap.put(TAG_EXPOSURE_DIFFERENCE, "Exposure Difference");
+        _tagNameMap.put(TAG_PREVIEW_IFD, "Preview IFD");
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_FLASH_USED, "Flash Used");
+        _tagNameMap.put(TAG_AF_FOCUS_POSITION, "AF Focus Position");
+        _tagNameMap.put(TAG_SHOOTING_MODE, "Shooting Mode");
+        _tagNameMap.put(TAG_LENS_STOPS, "Lens Stops");
+        _tagNameMap.put(TAG_CONTRAST_CURVE, "Contrast Curve");
+        _tagNameMap.put(TAG_LIGHT_SOURCE, "Light source");
+        _tagNameMap.put(TAG_SHOT_INFO, "Shot Info");
+        _tagNameMap.put(TAG_COLOR_BALANCE, "Color Balance");
+        _tagNameMap.put(TAG_LENS_DATA, "Lens Data");
+        _tagNameMap.put(TAG_NEF_THUMBNAIL_SIZE, "NEF Thumbnail Size");
+        _tagNameMap.put(TAG_SENSOR_PIXEL_SIZE, "Sensor Pixel Size");
+        _tagNameMap.put(TAG_UNKNOWN_10, "Unknown 10");
+        _tagNameMap.put(TAG_SCENE_ASSIST, "Scene Assist");
+        _tagNameMap.put(TAG_UNKNOWN_11, "Unknown 11");
+        _tagNameMap.put(TAG_RETOUCH_HISTORY, "Retouch History");
+        _tagNameMap.put(TAG_UNKNOWN_12, "Unknown 12");
+        _tagNameMap.put(TAG_FLASH_SYNC_MODE, "Flash Sync Mode");
+        _tagNameMap.put(TAG_AUTO_FLASH_MODE, "Auto Flash Mode");
+        _tagNameMap.put(TAG_AUTO_FLASH_COMPENSATION, "Auto Flash Compensation");
+        _tagNameMap.put(TAG_EXPOSURE_SEQUENCE_NUMBER, "Exposure Sequence Number");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+
+        _tagNameMap.put(TAG_UNKNOWN_20, "Unknown 20");
+        _tagNameMap.put(TAG_IMAGE_BOUNDARY, "Image Boundary");
+        _tagNameMap.put(TAG_FLASH_EXPOSURE_COMPENSATION, "Flash Exposure Compensation");
+        _tagNameMap.put(TAG_FLASH_BRACKET_COMPENSATION, "Flash Bracket Compensation");
+        _tagNameMap.put(TAG_AE_BRACKET_COMPENSATION, "AE Bracket Compensation");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_CROP_HIGH_SPEED, "Crop High Speed");
+        _tagNameMap.put(TAG_EXPOSURE_TUNING, "Exposure Tuning");
+        _tagNameMap.put(TAG_CAMERA_SERIAL_NUMBER, "Camera Serial Number");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_VR_INFO, "VR Info");
+        _tagNameMap.put(TAG_IMAGE_AUTHENTICATION, "Image Authentication");
+        _tagNameMap.put(TAG_UNKNOWN_35, "Unknown 35");
+        _tagNameMap.put(TAG_ACTIVE_D_LIGHTING, "Active D-Lighting");
+        _tagNameMap.put(TAG_PICTURE_CONTROL, "Picture Control");
+        _tagNameMap.put(TAG_WORLD_TIME, "World Time");
+        _tagNameMap.put(TAG_ISO_INFO, "ISO Info");
+        _tagNameMap.put(TAG_UNKNOWN_36, "Unknown 36");
+        _tagNameMap.put(TAG_UNKNOWN_37, "Unknown 37");
+        _tagNameMap.put(TAG_UNKNOWN_38, "Unknown 38");
+        _tagNameMap.put(TAG_UNKNOWN_39, "Unknown 39");
+        _tagNameMap.put(TAG_VIGNETTE_CONTROL, "Vignette Control");
+        _tagNameMap.put(TAG_UNKNOWN_40, "Unknown 40");
+        _tagNameMap.put(TAG_UNKNOWN_41, "Unknown 41");
+        _tagNameMap.put(TAG_UNKNOWN_42, "Unknown 42");
+        _tagNameMap.put(TAG_UNKNOWN_43, "Unknown 43");
+        _tagNameMap.put(TAG_UNKNOWN_44, "Unknown 44");
+        _tagNameMap.put(TAG_UNKNOWN_45, "Unknown 45");
+        _tagNameMap.put(TAG_UNKNOWN_46, "Unknown 46");
+        _tagNameMap.put(TAG_UNKNOWN_47, "Unknown 47");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+
+        _tagNameMap.put(TAG_CAMERA_SERIAL_NUMBER_2, "Camera Serial Number");
+        _tagNameMap.put(TAG_IMAGE_DATA_SIZE, "Image Data Size");
+        _tagNameMap.put(TAG_UNKNOWN_27, "Unknown 27");
+        _tagNameMap.put(TAG_UNKNOWN_28, "Unknown 28");
+        _tagNameMap.put(TAG_IMAGE_COUNT, "Image Count");
+        _tagNameMap.put(TAG_DELETED_IMAGE_COUNT, "Deleted Image Count");
+        _tagNameMap.put(TAG_SATURATION_2, "Saturation");
+        _tagNameMap.put(TAG_DIGITAL_VARI_PROGRAM, "Digital Vari Program");
+        _tagNameMap.put(TAG_IMAGE_STABILISATION, "Image Stabilisation");
+        _tagNameMap.put(TAG_AF_RESPONSE, "AF Response");
+        _tagNameMap.put(TAG_UNKNOWN_29, "Unknown 29");
+        _tagNameMap.put(TAG_UNKNOWN_30, "Unknown 30");
+        _tagNameMap.put(TAG_MULTI_EXPOSURE, "Multi Exposure");
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_UNKNOWN_31, "Unknown 31");
+        _tagNameMap.put(TAG_UNKNOWN_32, "Unknown 32");
+        _tagNameMap.put(TAG_UNKNOWN_33, "Unknown 33");
+        _tagNameMap.put(TAG_UNKNOWN_48, "Unknown 48");
+        _tagNameMap.put(TAG_POWER_UP_TIME, "Power Up Time");
+        _tagNameMap.put(TAG_AF_INFO_2, "AF Info 2");
+        _tagNameMap.put(TAG_FILE_INFO, "File Info");
+        _tagNameMap.put(TAG_AF_TUNE, "AF Tune");
+        _tagNameMap.put(TAG_FLASH_INFO, "Flash Info");
+        _tagNameMap.put(TAG_IMAGE_OPTIMISATION, "Image Optimisation");
+
+        _tagNameMap.put(TAG_IMAGE_ADJUSTMENT, "Image Adjustment");
+        _tagNameMap.put(TAG_CAMERA_TONE_COMPENSATION, "Tone Compensation");
+        _tagNameMap.put(TAG_ADAPTER, "Adapter");
+        _tagNameMap.put(TAG_LENS, "Lens");
+        _tagNameMap.put(TAG_MANUAL_FOCUS_DISTANCE, "Manual Focus Distance");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_CAMERA_COLOR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_CAMERA_HUE_ADJUSTMENT, "Camera Hue Adjustment");
+        _tagNameMap.put(TAG_NEF_COMPRESSION, "NEF Compression");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_LINEARIZATION_TABLE, "Linearization Table");
+        _tagNameMap.put(TAG_NIKON_CAPTURE_DATA, "Nikon Capture Data");
+        _tagNameMap.put(TAG_UNKNOWN_49, "Unknown 49");
+        _tagNameMap.put(TAG_UNKNOWN_50, "Unknown 50");
+        _tagNameMap.put(TAG_UNKNOWN_51, "Unknown 51");
+        _tagNameMap.put(TAG_PRINT_IM, "Print IM");
+        _tagNameMap.put(TAG_UNKNOWN_52, "Unknown 52");
+        _tagNameMap.put(TAG_UNKNOWN_53, "Unknown 53");
+        _tagNameMap.put(TAG_NIKON_CAPTURE_VERSION, "Nikon Capture Version");
+        _tagNameMap.put(TAG_NIKON_CAPTURE_OFFSETS, "Nikon Capture Offsets");
+        _tagNameMap.put(TAG_NIKON_SCAN, "Nikon Scan");
+        _tagNameMap.put(TAG_UNKNOWN_54, "Unknown 54");
+        _tagNameMap.put(TAG_NEF_BIT_DEPTH, "NEF Bit Depth");
+        _tagNameMap.put(TAG_UNKNOWN_55, "Unknown 55");
+    }
+
+    public NikonType2MakernoteDirectory()
+    {
+        this.setDescriptor(new NikonType2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Nikon Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,749 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.util.GregorianCalendar;
+
+import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link OlympusMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDirectory>
+{
+    // TODO extend support for some offset-encoded byte[] tags: http://www.ozhiker.com/electronics/pjmt/jpeg_info/olympus_mn.html
+
+    public OlympusMakernoteDescriptor(@NotNull OlympusMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case TAG_COLOUR_MODE:
+                return getColorModeDescription();
+            case TAG_IMAGE_QUALITY_1:
+                return getImageQuality1Description();
+            case TAG_IMAGE_QUALITY_2:
+                return getImageQuality2Description();
+            case TAG_SPECIAL_MODE:
+                return getSpecialModeDescription();
+            case TAG_JPEG_QUALITY:
+                return getJpegQualityDescription();
+            case TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case TAG_BW_MODE:
+                return getBWModeDescription();
+            case TAG_DIGI_ZOOM_RATIO:
+                return getDigiZoomRatioDescription();
+            case TAG_CAMERA_ID:
+                return getCameraIdDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FOCUS_RANGE:
+                return getFocusRangeDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+
+            case CameraSettings.TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case CameraSettings.TAG_FLASH_MODE:
+                return getFlashModeCameraSettingDescription();
+            case CameraSettings.TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case CameraSettings.TAG_IMAGE_SIZE:
+                return getImageSizeDescription();
+            case CameraSettings.TAG_IMAGE_QUALITY:
+                return getImageQualityDescription();
+            case CameraSettings.TAG_SHOOTING_MODE:
+                return getShootingModeDescription();
+            case CameraSettings.TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case CameraSettings.TAG_APEX_FILM_SPEED_VALUE:
+                return getApexFilmSpeedDescription();
+            case CameraSettings.TAG_APEX_SHUTTER_SPEED_TIME_VALUE:
+                return getApexShutterSpeedTimeDescription();
+            case CameraSettings.TAG_APEX_APERTURE_VALUE:
+                return getApexApertureDescription();
+            case CameraSettings.TAG_MACRO_MODE:
+                return getMacroModeCameraSettingDescription();
+            case CameraSettings.TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case CameraSettings.TAG_EXPOSURE_COMPENSATION:
+                return getExposureCompensationDescription();
+            case CameraSettings.TAG_BRACKET_STEP:
+                return getBracketStepDescription();
+
+            case CameraSettings.TAG_INTERVAL_LENGTH:
+                return getIntervalLengthDescription();
+            case CameraSettings.TAG_INTERVAL_NUMBER:
+                return getIntervalNumberDescription();
+            case CameraSettings.TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case CameraSettings.TAG_FOCUS_DISTANCE:
+                return getFocusDistanceDescription();
+            case CameraSettings.TAG_FLASH_FIRED:
+                return getFlastFiredDescription();
+            case CameraSettings.TAG_DATE:
+                return getDateDescription();
+            case CameraSettings.TAG_TIME:
+                return getTimeDescription();
+            case CameraSettings.TAG_MAX_APERTURE_AT_FOCAL_LENGTH:
+                return getMaxApertureAtFocalLengthDescription();
+
+            case CameraSettings.TAG_FILE_NUMBER_MEMORY:
+                return getFileNumberMemoryDescription();
+            case CameraSettings.TAG_LAST_FILE_NUMBER:
+                return getLastFileNumberDescription();
+            case CameraSettings.TAG_WHITE_BALANCE_RED:
+                return getWhiteBalanceRedDescription();
+            case CameraSettings.TAG_WHITE_BALANCE_GREEN:
+                return getWhiteBalanceGreenDescription();
+            case CameraSettings.TAG_WHITE_BALANCE_BLUE:
+                return getWhiteBalanceBlueDescription();
+            case CameraSettings.TAG_SATURATION:
+                return getSaturationDescription();
+            case CameraSettings.TAG_CONTRAST:
+                return getContrastDescription();
+            case CameraSettings.TAG_SHARPNESS:
+                return getSharpnessCameraSettingDescription();
+            case CameraSettings.TAG_SUBJECT_PROGRAM:
+                return getSubjectProgramDescription();
+            case CameraSettings.TAG_FLASH_COMPENSATION:
+                return getFlastCompensationDescription();
+            case CameraSettings.TAG_ISO_SETTING:
+                return getIsoSettingDescription();
+            case CameraSettings.TAG_CAMERA_MODEL:
+                return getCameraModelDescription();
+            case CameraSettings.TAG_INTERVAL_MODE:
+                return getIntervalModeDescription();
+            case CameraSettings.TAG_FOLDER_NAME:
+                return getFolderNameDescription();
+            case CameraSettings.TAG_COLOR_MODE:
+                return getColorModeCameraSettingDescription();
+            case CameraSettings.TAG_COLOR_FILTER:
+                return getColorFilterDescription();
+            case CameraSettings.TAG_BLACK_AND_WHITE_FILTER:
+                return getBlackAndWhiteFilterDescription();
+            case CameraSettings.TAG_INTERNAL_FLASH:
+                return getInternalFlashDescription();
+            case CameraSettings.TAG_APEX_BRIGHTNESS_VALUE:
+                return getApexBrightnessDescription();
+            case CameraSettings.TAG_SPOT_FOCUS_POINT_X_COORDINATE:
+                return getSpotFocusPointXCoordinateDescription();
+            case CameraSettings.TAG_SPOT_FOCUS_POINT_Y_COORDINATE:
+                return getSpotFocusPointYCoordinateDescription();
+            case CameraSettings.TAG_WIDE_FOCUS_ZONE:
+                return getWideFocusZoneDescription();
+            case CameraSettings.TAG_FOCUS_MODE:
+                return getFocusModeCameraSettingDescription();
+            case CameraSettings.TAG_FOCUS_AREA:
+                return getFocusAreaDescription();
+            case CameraSettings.TAG_DEC_SWITCH_POSITION:
+                return getDecSwitchPositionDescription();
+
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_EXPOSURE_MODE, "P", "A", "S", "M");
+    }
+
+    @Nullable
+    public String getFlashModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FLASH_MODE,
+            "Normal", "Red-eye reduction", "Rear flash sync", "Wireless");
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_WHITE_BALANCE,
+            "Auto", // 0
+            "Daylight",
+            "Cloudy",
+            "Tungsten",
+            null,
+            "Custom", // 5
+            null,
+            "Fluorescent",
+            "Fluorescent 2",
+            null,
+            null, // 10
+            "Custom 2",
+            "Custom 3"
+        );
+    }
+
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        // This is a pretty weird way to store this information!
+        return getIndexedDescription(CameraSettings.TAG_IMAGE_SIZE, "2560 x 1920", "1600 x 1200", "1280 x 960", "640 x 480");
+    }
+
+    @Nullable
+    public String getImageQualityDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_IMAGE_QUALITY, "Raw", "Super Fine", "Fine", "Standard", "Economy", "Extra Fine");
+    }
+
+    @Nullable
+    public String getShootingModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_SHOOTING_MODE,
+            "Single",
+            "Continuous",
+            "Self Timer",
+            null,
+            "Bracketing",
+            "Interval",
+            "UHS Continuous",
+            "HS Continuous"
+        );
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_METERING_MODE, "Multi-Segment", "Centre Weighted", "Spot");
+    }
+
+    @Nullable
+    public String getApexFilmSpeedDescription()
+    {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html#Minolta_Camera_Settings
+        // Apex Speed value = value/8-1 ,
+        // ISO = (2^(value/8-1))*3.125
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_FILM_SPEED_VALUE);
+
+        if (value == null)
+            return null;
+
+        double iso = Math.pow((value / 8d) - 1, 2) * 3.125;
+        return Double.toString(iso);
+    }
+
+    @Nullable
+    public String getApexShutterSpeedTimeDescription()
+    {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html#Minolta_Camera_Settings
+        // Apex Time value = value/8-6 ,
+        // ShutterSpeed = 2^( (48-value)/8 ),
+        // Due to rounding error value=8 should be displayed as 30 sec.
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_SHUTTER_SPEED_TIME_VALUE);
+
+        if (value == null)
+            return null;
+
+        double shutterSpeed = Math.pow((49-value) / 8d, 2);
+        return Double.toString(shutterSpeed) + " sec";
+    }
+
+    @Nullable
+    public String getApexApertureDescription()
+    {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html#Minolta_Camera_Settings
+        // Apex Aperture Value = value/8-1 ,
+        // Aperture F-stop = 2^( value/16-0.5 )
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_APERTURE_VALUE);
+
+        if (value == null)
+            return null;
+
+        double fStop = Math.pow((value/16d) - 0.5, 2);
+        return "F" + Double.toString(fStop);
+    }
+
+    @Nullable
+    public String getMacroModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_MACRO_MODE, "Off", "On");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "Off", "Electronic magnification", "Digital zoom 2x");
+    }
+
+    @Nullable
+    public String getExposureCompensationDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_EXPOSURE_COMPENSATION);
+        return value == null ? null : ((value / 3d) - 2) + " EV";
+    }
+
+    @Nullable
+    public String getBracketStepDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_BRACKET_STEP, "1/3 EV", "2/3 EV", "1 EV");
+    }
+
+    @Nullable
+    public String getIntervalLengthDescription()
+    {
+        if (!_directory.isIntervalMode())
+            return "N/A";
+
+        Long value = _directory.getLongObject(CameraSettings.TAG_INTERVAL_LENGTH);
+        return value == null ? null : value + " min";
+    }
+
+    @Nullable
+    public String getIntervalNumberDescription()
+    {
+        if (!_directory.isIntervalMode())
+            return "N/A";
+
+        Long value = _directory.getLongObject(CameraSettings.TAG_INTERVAL_NUMBER);
+        return value == null ? null : Long.toString(value);
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_FOCAL_LENGTH);
+        return value == null ? null : Double.toString(value/256d) + " mm";
+    }
+
+    @Nullable
+    public String getFocusDistanceDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_FOCUS_DISTANCE);
+        return value == null
+            ? null
+            : value == 0
+                ? "Infinity"
+                : value + " mm";
+    }
+
+    @Nullable
+    public String getFlastFiredDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FLASH_FIRED, "No", "Yes");
+    }
+
+    @Nullable
+    public String getDateDescription()
+    {
+        // day = value%256,
+        // month = floor( (value - floor( value/65536 )*65536 )/256 )
+        // year = floor( value/65536)
+        Long value = _directory.getLongObject(CameraSettings.TAG_DATE);
+        if (value == null)
+            return null;
+        long day = value & 0xFF;
+        long month = (value >> 16) & 0xFF;
+        long year = (value >> 8) & 0xFF;
+        return new GregorianCalendar((int)year + 1970, (int)month, (int)day).getTime().toString();
+    }
+
+    @Nullable
+    public String getTimeDescription()
+    {
+        // hours = floor( value/65536 ),
+        // minutes = floor( ( value - floor( value/65536 )*65536 )/256 ),
+        // seconds = value%256
+        Long value = _directory.getLongObject(CameraSettings.TAG_TIME);
+        if (value == null)
+            return null;
+        long hours = (value >> 8) & 0xFF;
+        long minutes = (value >> 16) & 0xFF;
+        long seconds = value & 0xFF;
+
+        return String.format("%02d:%02d:%02d", hours, minutes, seconds);
+    }
+
+    @Nullable
+    public String getMaxApertureAtFocalLengthDescription()
+    {
+        // Aperture F-Stop = 2^(value/16-0.5)
+        Long value = _directory.getLongObject(CameraSettings.TAG_TIME);
+        if (value == null)
+            return null;
+        double fStop = Math.pow((value/16d) - 0.5, 2);
+        return "F" + fStop;
+    }
+
+    @Nullable
+    public String getFileNumberMemoryDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FILE_NUMBER_MEMORY, "Off", "On");
+    }
+
+    @Nullable
+    public String getLastFileNumberDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_LAST_FILE_NUMBER);
+        return value == null
+            ? null
+            : value == 0
+                ? "File Number Memory Off"
+                : Long.toString(value);
+    }
+
+    @Nullable
+    public String getWhiteBalanceRedDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_RED);
+        return value == null ? null : Double.toString(value/256d);
+    }
+
+    @Nullable
+    public String getWhiteBalanceGreenDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_GREEN);
+        return value == null ? null : Double.toString(value/256d);
+    }
+
+    @Nullable
+    public String getWhiteBalanceBlueDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_BLUE);
+        return value == null ? null : Double.toString(value/256d);
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_SATURATION);
+        return value == null ? null : Long.toString(value-3);
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_CONTRAST);
+        return value == null ? null : Long.toString(value-3);
+    }
+
+    @Nullable
+    public String getSharpnessCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_SHARPNESS, "Hard", "Normal", "Soft");
+    }
+
+    @Nullable
+    public String getSubjectProgramDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_SUBJECT_PROGRAM, "None", "Portrait", "Text", "Night Portrait", "Sunset", "Sports Action");
+    }
+
+    @Nullable
+    public String getFlastCompensationDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_FLASH_COMPENSATION);
+        return value == null ? null : ((value-6)/3d) + " EV";
+    }
+
+    @Nullable
+    public String getIsoSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_ISO_SETTING, "100", "200", "400", "800", "Auto", "64");
+    }
+
+    @Nullable
+    public String getCameraModelDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_CAMERA_MODEL,
+            "DiMAGE 7",
+            "DiMAGE 5",
+            "DiMAGE S304",
+            "DiMAGE S404",
+            "DiMAGE 7i",
+            "DiMAGE 7Hi",
+            "DiMAGE A1",
+            "DiMAGE S414");
+    }
+
+    @Nullable
+    public String getIntervalModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_INTERVAL_MODE, "Still Image", "Time Lapse Movie");
+    }
+
+    @Nullable
+    public String getFolderNameDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOLDER_NAME, "Standard Form", "Data Form");
+    }
+
+    @Nullable
+    public String getColorModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_COLOR_MODE, "Natural Color", "Black & White", "Vivid Color", "Solarization", "AdobeRGB");
+    }
+
+    @Nullable
+    public String getColorFilterDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_COLOR_FILTER);
+        return value == null ? null : Long.toString(value-3);
+    }
+
+    @Nullable
+    public String getBlackAndWhiteFilterDescription()
+    {
+        return super.getDescription(CameraSettings.TAG_BLACK_AND_WHITE_FILTER);
+    }
+
+    @Nullable
+    public String getInternalFlashDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_INTERNAL_FLASH, "Did Not Fire", "Fired");
+    }
+
+    @Nullable
+    public String getApexBrightnessDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE);
+        return value == null ? null : Double.toString((value/8d)-6);
+    }
+
+    @Nullable
+    public String getSpotFocusPointXCoordinateDescription()
+    {
+        return super.getDescription(CameraSettings.TAG_SPOT_FOCUS_POINT_X_COORDINATE);
+    }
+
+    @Nullable
+    public String getSpotFocusPointYCoordinateDescription()
+    {
+        return super.getDescription(CameraSettings.TAG_SPOT_FOCUS_POINT_Y_COORDINATE);
+    }
+
+    @Nullable
+    public String getWideFocusZoneDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_WIDE_FOCUS_ZONE,
+            "No Zone or AF Failed",
+            "Center Zone (Horizontal Orientation)",
+            "Center Zone (Vertical Orientation)",
+            "Left Zone",
+            "Right Zone"
+        );
+    }
+
+    @Nullable
+    public String getFocusModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_MODE, "Auto Focus", "Manual Focus");
+    }
+
+    @Nullable
+    public String getFocusAreaDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_AREA, "Wide Focus (Normal)", "Spot Focus");
+    }
+
+    @Nullable
+    public String getDecSwitchPositionDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_DEC_SWITCH_POSITION, "Exposure", "Contrast", "Saturation", "Filter");
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getImageQuality2Description()
+    {
+        return getIndexedDescription(TAG_IMAGE_QUALITY_2,
+            "Raw",
+            "Super Fine",
+            "Fine",
+            "Standard",
+            "Extra Fine");
+    }
+
+    @Nullable
+    public String getImageQuality1Description()
+    {
+        return getIndexedDescription(TAG_IMAGE_QUALITY_1,
+            "Raw",
+            "Super Fine",
+            "Fine",
+            "Standard",
+            "Extra Fine");
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOUR_MODE,
+            "Natural Colour",
+            "Black & White",
+            "Vivid Colour",
+            "Solarization",
+            "AdobeRGB");
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal", "Hard", "Soft");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, "Auto", "Manual");
+    }
+
+    @Nullable
+    public String getFocusRangeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_RANGE, "Normal", "Macro");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE, null, null, "On", "Off");
+    }
+
+    @Nullable
+    public String getDigiZoomRatioDescription()
+    {
+        return getIndexedDescription(TAG_DIGI_ZOOM_RATIO, "Normal", null, "Digital 2x Zoom");
+    }
+
+    @Nullable
+    public String getCameraIdDescription()
+    {
+        byte[] bytes = _directory.getByteArray(TAG_CAMERA_ID);
+        if (bytes == null)
+            return null;
+        return new String(bytes);
+    }
+
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(TAG_MACRO_MODE, "Normal (no macro)", "Macro");
+    }
+
+    @Nullable
+    public String getBWModeDescription()
+    {
+        return getIndexedDescription(TAG_BW_MODE, "Off", "On");
+    }
+
+    @Nullable
+    public String getJpegQualityDescription()
+    {
+        return getIndexedDescription(TAG_JPEG_QUALITY,
+            1,
+            "Standard Quality",
+            "High Quality",
+            "Super High Quality");
+    }
+
+    @Nullable
+    public String getSpecialModeDescription()
+    {
+        long[] values = (long[])_directory.getObject(TAG_SPECIAL_MODE);
+        if (values==null)
+            return null;
+        if (values.length < 1)
+            return "";
+        StringBuilder desc = new StringBuilder();
+
+        switch ((int)values[0]) {
+            case 0:
+                desc.append("Normal picture taking mode");
+                break;
+            case 1:
+                desc.append("Unknown picture taking mode");
+                break;
+            case 2:
+                desc.append("Fast picture taking mode");
+                break;
+            case 3:
+                desc.append("Panorama picture taking mode");
+                break;
+            default:
+                desc.append("Unknown picture taking mode");
+                break;
+        }
+
+        if (values.length >= 2) {
+            switch ((int)values[1]) {
+                case 0:
+                    break;
+                case 1:
+                    desc.append(" / 1st in a sequence");
+                    break;
+                case 2:
+                    desc.append(" / 2nd in a sequence");
+                    break;
+                case 3:
+                    desc.append(" / 3rd in a sequence");
+                    break;
+                default:
+                    desc.append(" / ");
+                    desc.append(values[1]);
+                    desc.append("th in a sequence");
+                    break;
+            }
+        }
+        if (values.length >= 3) {
+            switch ((int)values[2]) {
+                case 1:
+                    desc.append(" / Left to right panorama direction");
+                    break;
+                case 2:
+                    desc.append(" / Right to left panorama direction");
+                    break;
+                case 3:
+                    desc.append(" / Bottom to top panorama direction");
+                    break;
+                case 4:
+                    desc.append(" / Top to bottom panorama direction");
+                    break;
+            }
+        }
+
+        return desc.toString();
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * The Olympus makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusMakernoteDirectory extends Directory
+{
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_MAKERNOTE_VERSION = 0x0000;
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_CAMERA_SETTINGS_1 = 0x0001;
+    /** Alternate Camera Settings Tag. Used by Konica / Minolta cameras. */
+    public static final int TAG_CAMERA_SETTINGS_2 = 0x0003;
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_COMPRESSED_IMAGE_SIZE = 0x0040;
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_MINOLTA_THUMBNAIL_OFFSET_1 = 0x0081;
+    /** Alternate Thumbnail Offset. Used by Konica / Minolta cameras. */
+    public static final int TAG_MINOLTA_THUMBNAIL_OFFSET_2 = 0x0088;
+    /** Length of thumbnail in bytes. Used by Konica / Minolta cameras. */
+    public static final int TAG_MINOLTA_THUMBNAIL_LENGTH = 0x0089;
+
+    /**
+     * Used by Konica / Minolta cameras
+     * 0 = Natural Colour
+     * 1 = Black &amp; White
+     * 2 = Vivid colour
+     * 3 = Solarization
+     * 4 = AdobeRGB
+     */
+    public static final int TAG_COLOUR_MODE = 0x0101;
+
+    /**
+     * Used by Konica / Minolta cameras.
+     * 0 = Raw
+     * 1 = Super Fine
+     * 2 = Fine
+     * 3 = Standard
+     * 4 = Extra Fine
+     */
+    public static final int TAG_IMAGE_QUALITY_1 = 0x0102;
+
+    /**
+     * Not 100% sure about this tag.
+     * <p>
+     * Used by Konica / Minolta cameras.
+     * 0 = Raw
+     * 1 = Super Fine
+     * 2 = Fine
+     * 3 = Standard
+     * 4 = Extra Fine
+     */
+    public static final int TAG_IMAGE_QUALITY_2 = 0x0103;
+
+
+    /**
+     * Three values:
+     * Value 1: 0=Normal, 2=Fast, 3=Panorama
+     * Value 2: Sequence Number Value 3:
+     * 1 = Panorama Direction: Left to Right
+     * 2 = Panorama Direction: Right to Left
+     * 3 = Panorama Direction: Bottom to Top
+     * 4 = Panorama Direction: Top to Bottom
+     */
+    public static final int TAG_SPECIAL_MODE = 0x0200;
+
+    /**
+     * 1 = Standard Quality
+     * 2 = High Quality
+     * 3 = Super High Quality
+     */
+    public static final int TAG_JPEG_QUALITY = 0x0201;
+
+    /**
+     * 0 = Normal (Not Macro)
+     * 1 = Macro
+     */
+    public static final int TAG_MACRO_MODE = 0x0202;
+
+    /**
+     * 0 = Off, 1 = On
+     */
+    public static final int TAG_BW_MODE = 0x0203;
+
+    /** Zoom Factor (0 or 1 = normal) */
+    public static final int TAG_DIGI_ZOOM_RATIO = 0x0204;
+    public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0205;
+    public static final int TAG_LENS_DISTORTION_PARAMETERS = 0x0206;
+    public static final int TAG_FIRMWARE_VERSION = 0x0207;
+    public static final int TAG_PICT_INFO = 0x0208;
+    public static final int TAG_CAMERA_ID = 0x0209;
+
+    /**
+     * Used by Epson cameras
+     * Units = pixels
+     */
+    public static final int TAG_IMAGE_WIDTH = 0x020B;
+
+    /**
+     * Used by Epson cameras
+     * Units = pixels
+     */
+    public static final int TAG_IMAGE_HEIGHT = 0x020C;
+
+    /** A string. Used by Epson cameras. */
+    public static final int TAG_ORIGINAL_MANUFACTURER_MODEL = 0x020D;
+
+    /**
+     * See the PIM specification here:
+     * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    public static final int TAG_DATA_DUMP = 0x0F00;
+
+    public static final int TAG_SHUTTER_SPEED_VALUE = 0x1000;
+    public static final int TAG_ISO_VALUE = 0x1001;
+    public static final int TAG_APERTURE_VALUE = 0x1002;
+    public static final int TAG_BRIGHTNESS_VALUE = 0x1003;
+    public static final int TAG_FLASH_MODE = 0x1004;
+    public static final int TAG_BRACKET = 0x1006;
+    public static final int TAG_FOCUS_RANGE = 0x100A;
+    public static final int TAG_FOCUS_MODE = 0x100B;
+    public static final int TAG_FOCUS_DISTANCE = 0x100C;
+    public static final int TAG_ZOOM = 0x100D;
+    public static final int TAG_MACRO_FOCUS = 0x100E;
+    public static final int TAG_SHARPNESS = 0x100F;
+    public static final int TAG_COLOUR_MATRIX = 0x1011;
+    public static final int TAG_BLACK_LEVEL = 0x1012;
+    public static final int TAG_WHITE_BALANCE = 0x1015;
+    public static final int TAG_RED_BIAS = 0x1017;
+    public static final int TAG_BLUE_BIAS = 0x1018;
+    public static final int TAG_SERIAL_NUMBER = 0x101A;
+    public static final int TAG_FLASH_BIAS = 0x1023;
+    public static final int TAG_CONTRAST = 0x1029;
+    public static final int TAG_SHARPNESS_FACTOR = 0x102A;
+    public static final int TAG_COLOUR_CONTROL = 0x102B;
+    public static final int TAG_VALID_BITS = 0x102C;
+    public static final int TAG_CORING_FILTER = 0x102D;
+    public static final int TAG_FINAL_WIDTH = 0x102E;
+    public static final int TAG_FINAL_HEIGHT = 0x102F;
+    public static final int TAG_COMPRESSION_RATIO = 0x1034;
+
+    public final static class CameraSettings
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the Makernote IFD
+        private static final int OFFSET = 0xF000;
+
+        public static final int TAG_EXPOSURE_MODE = OFFSET + 2;
+        public static final int TAG_FLASH_MODE = OFFSET + 3;
+        public static final int TAG_WHITE_BALANCE = OFFSET + 4;
+        public static final int TAG_IMAGE_SIZE = OFFSET + 5;
+        public static final int TAG_IMAGE_QUALITY = OFFSET + 6;
+        public static final int TAG_SHOOTING_MODE = OFFSET + 7;
+        public static final int TAG_METERING_MODE = OFFSET + 8;
+        public static final int TAG_APEX_FILM_SPEED_VALUE = OFFSET + 9;
+        public static final int TAG_APEX_SHUTTER_SPEED_TIME_VALUE = OFFSET + 10;
+        public static final int TAG_APEX_APERTURE_VALUE = OFFSET + 11;
+        public static final int TAG_MACRO_MODE = OFFSET + 12;
+        public static final int TAG_DIGITAL_ZOOM = OFFSET + 13;
+        public static final int TAG_EXPOSURE_COMPENSATION = OFFSET + 14;
+        public static final int TAG_BRACKET_STEP = OFFSET + 15;
+
+        public static final int TAG_INTERVAL_LENGTH = OFFSET + 17;
+        public static final int TAG_INTERVAL_NUMBER = OFFSET + 18;
+        public static final int TAG_FOCAL_LENGTH = OFFSET + 19;
+        public static final int TAG_FOCUS_DISTANCE = OFFSET + 20;
+        public static final int TAG_FLASH_FIRED = OFFSET + 21;
+        public static final int TAG_DATE = OFFSET + 22;
+        public static final int TAG_TIME = OFFSET + 23;
+        public static final int TAG_MAX_APERTURE_AT_FOCAL_LENGTH = OFFSET + 24;
+
+        public static final int TAG_FILE_NUMBER_MEMORY = OFFSET + 27;
+        public static final int TAG_LAST_FILE_NUMBER = OFFSET + 28;
+        public static final int TAG_WHITE_BALANCE_RED = OFFSET + 29;
+        public static final int TAG_WHITE_BALANCE_GREEN = OFFSET + 30;
+        public static final int TAG_WHITE_BALANCE_BLUE = OFFSET + 31;
+        public static final int TAG_SATURATION = OFFSET + 32;
+        public static final int TAG_CONTRAST = OFFSET + 33;
+        public static final int TAG_SHARPNESS = OFFSET + 34;
+        public static final int TAG_SUBJECT_PROGRAM = OFFSET + 35;
+        public static final int TAG_FLASH_COMPENSATION = OFFSET + 36;
+        public static final int TAG_ISO_SETTING = OFFSET + 37;
+        public static final int TAG_CAMERA_MODEL = OFFSET + 38;
+        public static final int TAG_INTERVAL_MODE = OFFSET + 39;
+        public static final int TAG_FOLDER_NAME = OFFSET + 40;
+        public static final int TAG_COLOR_MODE = OFFSET + 41;
+        public static final int TAG_COLOR_FILTER = OFFSET + 42;
+        public static final int TAG_BLACK_AND_WHITE_FILTER = OFFSET + 43;
+        public static final int TAG_INTERNAL_FLASH = OFFSET + 44;
+        public static final int TAG_APEX_BRIGHTNESS_VALUE = OFFSET + 45;
+        public static final int TAG_SPOT_FOCUS_POINT_X_COORDINATE = OFFSET + 46;
+        public static final int TAG_SPOT_FOCUS_POINT_Y_COORDINATE = OFFSET + 47;
+        public static final int TAG_WIDE_FOCUS_ZONE = OFFSET + 48;
+        public static final int TAG_FOCUS_MODE = OFFSET + 49;
+        public static final int TAG_FOCUS_AREA = OFFSET + 50;
+        public static final int TAG_DEC_SWITCH_POSITION = OFFSET + 51;
+    }
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode");
+        _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality");
+        _tagNameMap.put(TAG_MACRO_MODE, "Macro");
+        _tagNameMap.put(TAG_BW_MODE, "BW Mode");
+        _tagNameMap.put(TAG_DIGI_ZOOM_RATIO, "DigiZoom Ratio");
+        _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
+        _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters");
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_PICT_INFO, "Pict Info");
+        _tagNameMap.put(TAG_CAMERA_ID, "Camera Id");
+        _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS_1, "Camera Settings");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS_2, "Camera Settings");
+        _tagNameMap.put(TAG_COMPRESSED_IMAGE_SIZE, "Compressed Image Size");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_1, "Thumbnail Offset");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_2, "Thumbnail Offset");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_IMAGE_QUALITY_1, "Image Quality");
+        _tagNameMap.put(TAG_IMAGE_QUALITY_2, "Image Quality");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_ORIGINAL_MANUFACTURER_MODEL, "Original Manufacturer Model");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+
+        _tagNameMap.put(TAG_SHUTTER_SPEED_VALUE, "Shutter Speed Value");
+        _tagNameMap.put(TAG_ISO_VALUE, "ISO Value");
+        _tagNameMap.put(TAG_APERTURE_VALUE, "Aperture Value");
+        _tagNameMap.put(TAG_BRIGHTNESS_VALUE, "Brightness Value");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_BRACKET, "Bracket");
+        _tagNameMap.put(TAG_FOCUS_RANGE, "Focus Range");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FOCUS_DISTANCE, "Focus Distance");
+        _tagNameMap.put(TAG_ZOOM, "Zoom");
+        _tagNameMap.put(TAG_MACRO_FOCUS, "Macro Focus");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_COLOUR_MATRIX, "Colour Matrix");
+        _tagNameMap.put(TAG_BLACK_LEVEL, "Black Level");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_RED_BIAS, "Red Bias");
+        _tagNameMap.put(TAG_BLUE_BIAS, "Blue Bias");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SHARPNESS_FACTOR, "Sharpness Factor");
+        _tagNameMap.put(TAG_COLOUR_CONTROL, "Colour Control");
+        _tagNameMap.put(TAG_VALID_BITS, "Valid Bits");
+        _tagNameMap.put(TAG_CORING_FILTER, "Coring Filter");
+        _tagNameMap.put(TAG_FINAL_WIDTH, "Final Width");
+        _tagNameMap.put(TAG_FINAL_HEIGHT, "Final Height");
+        _tagNameMap.put(TAG_COMPRESSION_RATIO, "Compression Ratio");
+
+        _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(CameraSettings.TAG_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(CameraSettings.TAG_IMAGE_QUALITY, "Image Quality");
+        _tagNameMap.put(CameraSettings.TAG_SHOOTING_MODE, "Shooting Mode");
+        _tagNameMap.put(CameraSettings.TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(CameraSettings.TAG_APEX_FILM_SPEED_VALUE, "Apex Film Speed Value");
+        _tagNameMap.put(CameraSettings.TAG_APEX_SHUTTER_SPEED_TIME_VALUE, "Apex Shutter Speed Time Value");
+        _tagNameMap.put(CameraSettings.TAG_APEX_APERTURE_VALUE, "Apex Aperture Value");
+        _tagNameMap.put(CameraSettings.TAG_MACRO_MODE, "Macro Mode");
+        _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(CameraSettings.TAG_EXPOSURE_COMPENSATION, "Exposure Compensation");
+        _tagNameMap.put(CameraSettings.TAG_BRACKET_STEP, "Bracket Step");
+
+        _tagNameMap.put(CameraSettings.TAG_INTERVAL_LENGTH, "Interval Length");
+        _tagNameMap.put(CameraSettings.TAG_INTERVAL_NUMBER, "Interval Number");
+        _tagNameMap.put(CameraSettings.TAG_FOCAL_LENGTH, "Focal Length");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_DISTANCE, "Focus Distance");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_FIRED, "Flash Fired");
+        _tagNameMap.put(CameraSettings.TAG_DATE, "Date");
+        _tagNameMap.put(CameraSettings.TAG_TIME, "Time");
+        _tagNameMap.put(CameraSettings.TAG_MAX_APERTURE_AT_FOCAL_LENGTH, "Max Aperture at Focal Length");
+
+        _tagNameMap.put(CameraSettings.TAG_FILE_NUMBER_MEMORY, "File Number Memory");
+        _tagNameMap.put(CameraSettings.TAG_LAST_FILE_NUMBER, "Last File Number");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE_RED, "White Balance Red");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE_GREEN, "White Balance Green");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE_BLUE, "White Balance Blue");
+        _tagNameMap.put(CameraSettings.TAG_SATURATION, "Saturation");
+        _tagNameMap.put(CameraSettings.TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(CameraSettings.TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(CameraSettings.TAG_SUBJECT_PROGRAM, "Subject Program");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_COMPENSATION, "Flash Compensation");
+        _tagNameMap.put(CameraSettings.TAG_ISO_SETTING, "ISO Setting");
+        _tagNameMap.put(CameraSettings.TAG_CAMERA_MODEL, "Camera Model");
+        _tagNameMap.put(CameraSettings.TAG_INTERVAL_MODE, "Interval Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOLDER_NAME, "Folder Name");
+        _tagNameMap.put(CameraSettings.TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(CameraSettings.TAG_COLOR_FILTER, "Color Filter");
+        _tagNameMap.put(CameraSettings.TAG_BLACK_AND_WHITE_FILTER, "Black and White Filter");
+        _tagNameMap.put(CameraSettings.TAG_INTERNAL_FLASH, "Internal Flash");
+        _tagNameMap.put(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE, "Apex Brightness Value");
+        _tagNameMap.put(CameraSettings.TAG_SPOT_FOCUS_POINT_X_COORDINATE, "Spot Focus Point X Coordinate");
+        _tagNameMap.put(CameraSettings.TAG_SPOT_FOCUS_POINT_Y_COORDINATE, "Spot Focus Point Y Coordinate");
+        _tagNameMap.put(CameraSettings.TAG_WIDE_FOCUS_ZONE, "Wide Focus Zone");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_AREA, "Focus Area");
+        _tagNameMap.put(CameraSettings.TAG_DEC_SWITCH_POSITION, "DEC Switch Position");
+    }
+
+    public OlympusMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Makernote";
+    }
+
+    @Override
+    public void setByteArray(int tagType, @NotNull byte[] bytes)
+    {
+        if (tagType == TAG_CAMERA_SETTINGS_1 || tagType == TAG_CAMERA_SETTINGS_2) {
+            processCameraSettings(bytes);
+        } else {
+            super.setByteArray(tagType, bytes);
+        }
+    }
+
+    private void processCameraSettings(byte[] bytes)
+    {
+        SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes);
+        reader.setMotorolaByteOrder(true);
+
+        int count = bytes.length / 4;
+
+        try {
+            for (int i = 0; i < count; i++) {
+                int value = reader.getInt32();
+                setInt(CameraSettings.OFFSET + i, value);
+            }
+        } catch (IOException e) {
+            // Should never happen, given that we check the length of the bytes beforehand.
+            e.printStackTrace();
+        }
+    }
+
+    public boolean isIntervalMode()
+    {
+        Long value = getLongObject(CameraSettings.TAG_SHOOTING_MODE);
+        return value != null && value == 5;
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+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.IOException;
+
+import static com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicMakernoteDirectory}.
+ * <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>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Philipp Sandhaus
+ */
+public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory>
+{
+    public PanasonicMakernoteDescriptor(@NotNull PanasonicMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY_MODE:
+                return getQualityModeDescription();
+            case TAG_FIRMWARE_VERSION:
+                return getVersionDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_AF_AREA_MODE:
+                return getAfAreaModeDescription();
+            case TAG_IMAGE_STABILIZATION:
+                return getImageStabilizationDescription();
+            case TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case TAG_RECORD_MODE:
+                return getRecordModeDescription();
+            case TAG_AUDIO:
+                return getAudioDescription();
+            case TAG_UNKNOWN_DATA_DUMP:
+                return getUnknownDataDumpDescription();
+            case TAG_COLOR_EFFECT:
+                return getColorEffectDescription();
+            case TAG_UPTIME:
+                return getUptimeDescription();
+            case TAG_BURST_MODE:
+                return getBurstModeDescription();
+            case TAG_CONTRAST_MODE:
+                return getContrastModeDescription();
+            case TAG_NOISE_REDUCTION:
+                return getNoiseReductionDescription();
+            case TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case TAG_ROTATION:
+                return getRotationDescription();
+            case TAG_AF_ASSIST_LAMP:
+                return getAfAssistLampDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_OPTICAL_ZOOM_MODE:
+                return getOpticalZoomModeDescription();
+            case TAG_CONVERSION_LENS:
+                return getConversionLensDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_WORLD_TIME_LOCATION:
+                return getWorldTimeLocationDescription();
+            case TAG_ADVANCED_SCENE_MODE:
+                return getAdvancedSceneModeDescription();
+            case TAG_FACE_DETECTION_INFO:
+                return getDetectedFacesDescription();
+            case TAG_TRANSFORM:
+                return getTransformDescription();
+			case TAG_TRANSFORM_1:
+	            return getTransform1Description();
+            case TAG_INTELLIGENT_EXPOSURE:
+                return getIntelligentExposureDescription();
+            case TAG_FLASH_WARNING:
+                return getFlashWarningDescription();
+            case TAG_COUNTRY:
+                return getCountryDescription();
+            case TAG_STATE:
+                return getStateDescription();
+            case TAG_CITY:
+                return getCityDescription();
+            case TAG_LANDMARK:
+                return getLandmarkDescription();
+            case TAG_INTELLIGENT_RESOLUTION:
+                return getIntelligentResolutionDescription();
+            case TAG_FACE_RECOGNITION_INFO:
+                return getRecognizedFacesDescription();
+            case TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case TAG_SCENE_MODE:
+                return getSceneModeDescription();
+            case TAG_FLASH_FIRED:
+                return getFlashFiredDescription();
+            case TAG_TEXT_STAMP:
+		        return getTextStampDescription();
+			case TAG_TEXT_STAMP_1:
+	             return getTextStamp1Description();
+			case TAG_TEXT_STAMP_2:
+		         return getTextStamp2Description();
+			case TAG_TEXT_STAMP_3:
+			     return getTextStamp3Description();
+            case TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case TAG_INTERNAL_SERIAL_NUMBER:
+                return getInternalSerialNumberDescription();
+            case TAG_TITLE:
+	            return getTitleDescription();
+			case TAG_BABY_NAME:
+	            return getBabyNameDescription();
+			case TAG_LOCATION:
+	            return getLocationDescription();
+			case TAG_BABY_AGE:
+		        return getBabyAgeDescription();
+			case TAG_BABY_AGE_1:
+		        return getBabyAge1Description();
+			default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
+    {
+        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
+    }
+
+    @Nullable
+    public String getTextStampDescription()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getTextStamp1Description()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP_1, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getTextStamp2Description()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP_2, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getTextStamp3Description()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP_3, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(TAG_MACRO_MODE, 1, "Off", "On");
+    }
+
+    @Nullable
+    public String getFlashFiredDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_FIRED, 1, "Off", "On");
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_STABILIZATION,
+            2,
+            "On, Mode 1",
+            "Off",
+            "On, Mode 2"
+        );
+    }
+
+    @Nullable
+    public String getAudioDescription()
+    {
+        return getIndexedDescription(TAG_AUDIO, 1, "Off", "On");
+    }
+
+    @Nullable
+    public String getTransformDescription()
+    {
+        return getTransformDescription(TAG_TRANSFORM);
+    }
+
+    @Nullable
+    public String getTransform1Description()
+    {
+        return getTransformDescription(TAG_TRANSFORM_1);
+    }
+
+    @Nullable
+    private String getTransformDescription(int tag)
+    {
+        byte[] values = _directory.getByteArray(tag);
+        if (values == null)
+            return null;
+
+        RandomAccessReader reader = new ByteArrayReader(values);
+
+        try
+        {
+            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 (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getIntelligentExposureDescription()
+    {
+        return getIndexedDescription(TAG_INTELLIGENT_EXPOSURE,
+            "Off", "Low", "Standard", "High");
+    }
+
+    @Nullable
+    public String getFlashWarningDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_WARNING,
+            "No", "Yes (Flash required but disabled)");
+    }
+
+    @Nullable
+    public String getCountryDescription()
+    {
+        return getAsciiStringFromBytes(TAG_COUNTRY);
+    }
+
+    @Nullable
+    public String getStateDescription()
+    {
+        return getAsciiStringFromBytes(TAG_STATE);
+    }
+
+    @Nullable
+    public String getCityDescription()
+    {
+        return getAsciiStringFromBytes(TAG_CITY);
+    }
+
+    @Nullable
+    public String getLandmarkDescription()
+    {
+        return getAsciiStringFromBytes(TAG_LANDMARK);
+    }
+
+	@Nullable
+    public String getTitleDescription()
+    {
+        return getAsciiStringFromBytes(TAG_TITLE);
+    }
+
+	@Nullable
+    public String getBabyNameDescription()
+    {
+        return getAsciiStringFromBytes(TAG_BABY_NAME);
+    }
+
+	@Nullable
+    public String getLocationDescription()
+    {
+        return getAsciiStringFromBytes(TAG_LOCATION);
+    }
+
+    @Nullable
+    public String getIntelligentResolutionDescription()
+    {
+        return getIndexedDescription(TAG_INTELLIGENT_RESOLUTION,
+            "Off", null, "Auto", "On");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "Normal");
+    }
+
+    @Nullable
+    public String getWorldTimeLocationDescription()
+    {
+        return getIndexedDescription(TAG_WORLD_TIME_LOCATION,
+            1, "Home", "Destination");
+    }
+
+    @Nullable
+    public String getAdvancedSceneModeDescription()
+    {
+        return getIndexedDescription(TAG_ADVANCED_SCENE_MODE,
+            1,
+            "Normal",
+            "Outdoor/Illuminations/Flower/HDR Art",
+            "Indoor/Architecture/Objects/HDR B&W",
+            "Creative",
+            "Auto",
+            null,
+            "Expressive",
+            "Retro",
+            "Pure",
+            "Elegant",
+            null,
+            "Monochrome",
+            "Dynamic Art",
+            "Silhouette"
+        );
+    }
+
+    @Nullable
+    public String getUnknownDataDumpDescription()
+    {
+        return getByteLengthDescription(TAG_UNKNOWN_DATA_DUMP);
+    }
+
+    @Nullable
+    public String getColorEffectDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_EFFECT,
+            1, "Off", "Warm", "Cool", "Black & White", "Sepia"
+        );
+    }
+
+    @Nullable
+    public String getUptimeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_UPTIME);
+        if (value == null)
+            return null;
+        return value / 100f + " s";
+    }
+
+    @Nullable
+    public String getBurstModeDescription()
+    {
+        return getIndexedDescription(TAG_BURST_MODE,
+            "Off", null, "On", "Indefinite", "Unlimited"
+        );
+    }
+
+    @Nullable
+    public String getContrastModeDescription()
+    {
+        Integer value = _directory.getInteger(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()
+    {
+        return getIndexedDescription(TAG_NOISE_REDUCTION,
+            "Standard (0)", "Low (-1)", "High (+1)", "Lowest (-2)", "Highest (+2)"
+        );
+    }
+
+    @Nullable
+    public String getSelfTimerDescription()
+    {
+        return getIndexedDescription(TAG_SELF_TIMER,
+            1, "Off", "10 s", "2 s"
+        );
+    }
+
+    @Nullable
+    public String getRotationDescription()
+    {
+        Integer value = _directory.getInteger(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()
+    {
+        return getIndexedDescription(TAG_AF_ASSIST_LAMP,
+            1, "Fired", "Enabled but not used", "Disabled but required", "Disabled and not required"
+        );
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_MODE,
+            "Normal", "Natural", "Vivid"
+        );
+    }
+
+    @Nullable
+    public String getOpticalZoomModeDescription()
+    {
+        return getIndexedDescription(TAG_OPTICAL_ZOOM_MODE,
+            1, "Standard", "Extended"
+        );
+    }
+
+    @Nullable
+    public String getConversionLensDescription()
+    {
+        return getIndexedDescription(TAG_CONVERSION_LENS,
+            1, "Off", "Wide", "Telephoto", "Macro"
+        );
+    }
+
+    @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");
+
+        return result.length() > 0 ? result.substring(0, result.length() - 1) : null;
+
+    }
+
+    private static final String[] _sceneModes = new String[] {
+        "Normal", // 1
+        "Portrait",
+        "Scenery",
+        "Sports",
+        "Night Portrait",
+        "Program",
+        "Aperture Priority",
+        "Shutter Priority",
+        "Macro",
+        "Spot", // 10
+        "Manual",
+        "Movie Preview",
+        "Panning",
+        "Simple",
+        "Color Effects",
+        "Self Portrait",
+        "Economy",
+        "Fireworks",
+        "Party",
+        "Snow", // 20
+        "Night Scenery",
+        "Food",
+        "Baby",
+        "Soft Skin",
+        "Candlelight",
+        "Starry Night",
+        "High Sensitivity",
+        "Panorama Assist",
+        "Underwater",
+        "Beach", // 30
+        "Aerial Photo",
+        "Sunset",
+        "Pet",
+        "Intelligent ISO",
+        "Clipboard",
+        "High Speed Continuous Shooting",
+        "Intelligent Auto",
+        null,
+        "Multi-aspect",
+        null, // 40
+        "Transform",
+        "Flash Burst",
+        "Pin Hole",
+        "Film Grain",
+        "My Color",
+        "Photo Frame",
+        null,
+        null,
+        null,
+        null, // 50
+        "HDR"
+    };
+
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        return getIndexedDescription(TAG_RECORD_MODE, 1, _sceneModes);
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_MODE, 1, _sceneModes);
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, 1,
+            "Auto", "Manual", null, "Auto, Focus Button", "Auto, Continuous");
+    }
+
+    @Nullable
+    public String getAfAreaModeDescription()
+    {
+        int[] value = _directory.getIntArray(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()
+    {
+        return getIndexedDescription(TAG_QUALITY_MODE,
+            2,
+            "High", // 2
+            "Normal",
+            null,
+            null,
+            "Very High",
+            "Raw",
+            null,
+            "Motion Picture" // 9
+        );
+    }
+
+    @Nullable
+    public String getVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FIRMWARE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
+    }
+
+    @Nullable
+    public String getInternalSerialNumberDescription()
+    {
+        return get7BitStringFromBytes(TAG_INTERNAL_SERIAL_NUMBER);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            1,
+            "Auto", // 1
+            "Daylight",
+            "Cloudy",
+            "Incandescent",
+            "Manual",
+            null,
+            null,
+            "Flash",
+            null,
+            "Black & White", // 10
+            "Manual",
+            "Shade" // 12
+        );
+    }
+
+	@Nullable
+	public String getBabyAgeDescription()
+    {
+        final Age age = _directory.getAge(TAG_BABY_AGE);
+        return age == null ? null : age.toFriendlyString();
+    }
+
+	@Nullable
+	public String getBabyAge1Description()
+    {
+        final Age age = _directory.getAge(TAG_BABY_AGE_1);
+        return age == null ? null : age.toFriendlyString();
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,643 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+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.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Panasonic and Leica cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com, Philipp Sandhaus
+ */
+public class PanasonicMakernoteDirectory extends Directory
+{
+
+    /**
+     * <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_FIRMWARE_VERSION = 0x0002;
+
+    /**
+     * <br>
+     * 1 = Auto            <br>
+     * 2 = Daylight        <br>
+     * 3 = Cloudy          <br>
+     * 4 = Incandescent    <br>
+     * 5 = Manual          <br>
+     * 8 = Flash           <br>
+     * 10 = Black &amp; 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_EASY_MODE = 0x0022;
+    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 &amp; 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&amp;W Film)                                 <br>
+     * 22 = Smooth (B&amp;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&amp;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(TAG_QUALITY_MODE, "Quality Mode");
+        _tagNameMap.put(TAG_FIRMWARE_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_EASY_MODE, "Easy Mode");
+        _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");
+    }
+
+    public PanasonicMakernoteDirectory()
+    {
+        this.setDescriptor(new PanasonicMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Panasonic Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    @Nullable
+    public Face[] getDetectedFaces()
+    {
+        byte[] bytes = getByteArray(TAG_FACE_DETECTION_INFO);
+        if (bytes==null)
+            return null;
+
+        RandomAccessReader 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 (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public Face[] getRecognizedFaces()
+    {
+        byte[] bytes = getByteArray(TAG_FACE_RECOGNITION_INFO);
+        if (bytes == null)
+            return null;
+
+        RandomAccessReader 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 (IOException 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/makernotes/PentaxMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.PentaxMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PentaxMakernoteDirectory}.
+ * <p>
+ * Some information about this makernote taken from here:
+ * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pentax_mn.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PentaxMakernoteDescriptor extends TagDescriptor<PentaxMakernoteDirectory>
+{
+    public PentaxMakernoteDescriptor(@NotNull PentaxMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_CAPTURE_MODE:
+                return getCaptureModeDescription();
+            case TAG_QUALITY_LEVEL:
+                return getQualityLevelDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_ISO_SPEED:
+                return getIsoSpeedDescription();
+            case TAG_COLOUR:
+                return getColourDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getColourDescription()
+    {
+        return getIndexedDescription(TAG_COLOUR, 1, "Normal", "Black & White", "Sepia");
+    }
+
+    @Nullable
+    public String getIsoSpeedDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ISO_SPEED);
+        if (value == null)
+            return null;
+        switch (value) {
+            // TODO there must be other values which aren't catered for here
+            case 10: return "ISO 100";
+            case 16: return "ISO 200";
+            case 100: return "ISO 100";
+            case 200: return "ISO 200";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal", "Soft", "Hard");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Float value = _directory.getFloatObject(TAG_DIGITAL_ZOOM);
+        if (value == null)
+            return null;
+        if (value == 0)
+            return "Off";
+        return Float.toString(value);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            "Auto", "Daylight", "Shade", "Tungsten", "Fluorescent", "Manual");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE,
+            1, "Auto", "Flash On", null, "Flash Off", null, "Red-eye Reduction");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, 2, "Custom", "Auto");
+    }
+
+    @Nullable
+    public String getQualityLevelDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY_LEVEL, "Good", "Better", "Best");
+    }
+
+    @Nullable
+    public String getCaptureModeDescription()
+    {
+        return getIndexedDescription(TAG_CAPTURE_MODE,
+            "Auto", "Night-scene", "Manual", null, "Multiple");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Pentax and Asahi cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PentaxMakernoteDirectory extends Directory
+{
+    /**
+     * 0 = Auto
+     * 1 = Night-scene
+     * 2 = Manual
+     * 4 = Multiple
+     */
+    public static final int TAG_CAPTURE_MODE = 0x0001;
+
+    /**
+     * 0 = Good
+     * 1 = Better
+     * 2 = Best
+     */
+    public static final int TAG_QUALITY_LEVEL = 0x0002;
+
+    /**
+     * 2 = Custom
+     * 3 = Auto
+     */
+    public static final int TAG_FOCUS_MODE = 0x0003;
+
+    /**
+     * 1 = Auto
+     * 2 = Flash on
+     * 4 = Flash off
+     * 6 = Red-eye Reduction
+     */
+    public static final int TAG_FLASH_MODE = 0x0004;
+
+    /**
+     * 0 = Auto
+     * 1 = Daylight
+     * 2 = Shade
+     * 3 = Tungsten
+     * 4 = Fluorescent
+     * 5 = Manual
+     */
+    public static final int TAG_WHITE_BALANCE = 0x0007;
+
+    /**
+     * (0 = Off)
+     */
+    public static final int TAG_DIGITAL_ZOOM = 0x000A;
+
+    /**
+     * 0 = Normal
+     * 1 = Soft
+     * 2 = Hard
+     */
+    public static final int TAG_SHARPNESS = 0x000B;
+
+    /**
+     * 0 = Normal
+     * 1 = Low
+     * 2 = High
+     */
+    public static final int TAG_CONTRAST = 0x000C;
+
+    /**
+     * 0 = Normal
+     * 1 = Low
+     * 2 = High
+     */
+    public static final int TAG_SATURATION = 0x000D;
+
+    /**
+     * 10 = ISO 100
+     * 16 = ISO 200
+     * 100 = ISO 100
+     * 200 = ISO 200
+     */
+    public static final int TAG_ISO_SPEED = 0x0014;
+
+    /**
+     * 1 = Normal
+     * 2 = Black &amp; White
+     * 3 = Sepia
+     */
+    public static final int TAG_COLOUR = 0x0017;
+
+    /**
+     * See Print Image Matching for specification.
+     * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    /**
+     * (String).
+     */
+    public static final int TAG_TIME_ZONE = 0x1000;
+
+    /**
+     * (String).
+     */
+    public static final int TAG_DAYLIGHT_SAVINGS = 0x1001;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CAPTURE_MODE, "Capture Mode");
+        _tagNameMap.put(TAG_QUALITY_LEVEL, "Quality Level");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_ISO_SPEED, "ISO Speed");
+        _tagNameMap.put(TAG_COLOUR, "Colour");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_TIME_ZONE, "Time Zone");
+        _tagNameMap.put(TAG_DAYLIGHT_SAVINGS, "Daylight Savings");
+    }
+
+    public PentaxMakernoteDirectory()
+    {
+        this.setDescriptor(new PentaxMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Pentax Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+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 {@link RicohMakernoteDescriptor}.
+ * <p>
+ * Some information about this makernote taken from here:
+ * http://www.ozhiker.com/electronics/pjmt/jpeg_info/ricoh_mn.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirectory>
+{
+    public RicohMakernoteDescriptor(@NotNull RicohMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+//            case TAG_PRINT_IMAGE_MATCHING_INFO:
+//                return getPrintImageMatchingInfoDescription();
+//            case TAG_PROPRIETARY_THUMBNAIL:
+//                return getProprietaryThumbnailDataDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+//    @Nullable
+//    public String getPrintImageMatchingInfoDescription()
+//    {
+//        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
+//    }
+//
+//    @Nullable
+//    public String getProprietaryThumbnailDataDescription()
+//    {
+//        return getByteLengthDescription(TAG_PROPRIETARY_THUMBNAIL);
+//    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Ricoh cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RicohMakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_DATA_TYPE = 0x0001;
+    public static final int TAG_VERSION = 0x0002;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    public static final int TAG_RICOH_CAMERA_INFO_MAKERNOTE_SUB_IFD_POINTER = 0x2001;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_DATA_TYPE, "Makernote Data Type");
+        _tagNameMap.put(TAG_VERSION, "Version");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_RICOH_CAMERA_INFO_MAKERNOTE_SUB_IFD_POINTER, "Ricoh Camera Info Makernote Sub-IFD");
+    }
+
+    public RicohMakernoteDirectory()
+    {
+        this.setDescriptor(new RicohMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Ricoh Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SanyoMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SanyoMakernoteDescriptor extends TagDescriptor<SanyoMakernoteDirectory>
+{
+    public SanyoMakernoteDescriptor(@NotNull SanyoMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_SANYO_QUALITY:
+                return getSanyoQualityDescription();
+            case TAG_MACRO:
+                return getMacroDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_SEQUENTIAL_SHOT:
+                return getSequentialShotDescription();
+            case TAG_WIDE_RANGE:
+                return getWideRangeDescription();
+            case TAG_COLOR_ADJUSTMENT_MODE:
+                return getColorAdjustmentModeDescription();
+            case TAG_QUICK_SHOT:
+                return getQuickShotDescription();
+            case TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case TAG_VOICE_MEMO:
+                return getVoiceMemoDescription();
+            case TAG_RECORD_SHUTTER_RELEASE:
+                return getRecordShutterDescription();
+            case TAG_FLICKER_REDUCE:
+                return getFlickerReduceDescription();
+            case TAG_OPTICAL_ZOOM_ON:
+                return getOptimalZoomOnDescription();
+            case TAG_DIGITAL_ZOOM_ON:
+                return getDigitalZoomOnDescription();
+            case TAG_LIGHT_SOURCE_SPECIAL:
+                return getLightSourceSpecialDescription();
+            case TAG_RESAVED:
+                return getResavedDescription();
+            case TAG_SCENE_SELECT:
+                return getSceneSelectDescription();
+            case TAG_SEQUENCE_SHOT_INTERVAL:
+                return getSequenceShotIntervalDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSanyoQualityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SANYO_QUALITY);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x0: return "Normal/Very Low";
+            case 0x1: return "Normal/Low";
+            case 0x2: return "Normal/Medium Low";
+            case 0x3: return "Normal/Medium";
+            case 0x4: return "Normal/Medium High";
+            case 0x5: return "Normal/High";
+            case 0x6: return "Normal/Very High";
+            case 0x7: return "Normal/Super High";
+            case 0x100: return "Fine/Very Low";
+            case 0x101: return "Fine/Low";
+            case 0x102: return "Fine/Medium Low";
+            case 0x103: return "Fine/Medium";
+            case 0x104: return "Fine/Medium High";
+            case 0x105: return "Fine/High";
+            case 0x106: return "Fine/Very High";
+            case 0x107: return "Fine/Super High";
+            case 0x200: return "Super Fine/Very Low";
+            case 0x201: return "Super Fine/Low";
+            case 0x202: return "Super Fine/Medium Low";
+            case 0x203: return "Super Fine/Medium";
+            case 0x204: return "Super Fine/Medium High";
+            case 0x205: return "Super Fine/High";
+            case 0x206: return "Super Fine/Very High";
+            case 0x207: return "Super Fine/Super High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    private String getMacroDescription()
+    {
+        return getIndexedDescription(TAG_MACRO, "Normal", "Macro", "View", "Manual");
+    }
+
+    @Nullable
+    private String getDigitalZoomDescription()
+    {
+        return getDecimalRational(TAG_DIGITAL_ZOOM, 3);
+    }
+
+    @Nullable
+    private String getSequentialShotDescription()
+    {
+        return getIndexedDescription(TAG_SEQUENTIAL_SHOT, "None", "Standard", "Best", "Adjust Exposure");
+    }
+
+    @Nullable
+    private String getWideRangeDescription()
+    {
+        return getIndexedDescription(TAG_WIDE_RANGE, "Off", "On");
+    }
+
+    @Nullable
+    private String getColorAdjustmentModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_ADJUSTMENT_MODE, "Off", "On");
+    }
+
+    @Nullable
+    private String getQuickShotDescription()
+    {
+        return getIndexedDescription(TAG_QUICK_SHOT, "Off", "On");
+    }
+
+    @Nullable
+    private String getSelfTimerDescription()
+    {
+        return getIndexedDescription(TAG_SELF_TIMER, "Off", "On");
+    }
+
+    @Nullable
+    private String getVoiceMemoDescription()
+    {
+        return getIndexedDescription(TAG_VOICE_MEMO, "Off", "On");
+    }
+
+    @Nullable
+    private String getRecordShutterDescription()
+    {
+        return getIndexedDescription(TAG_RECORD_SHUTTER_RELEASE, "Record while down", "Press start, press stop");
+    }
+
+    @Nullable
+    private String getFlickerReduceDescription()
+    {
+        return getIndexedDescription(TAG_FLICKER_REDUCE, "Off", "On");
+    }
+
+    @Nullable
+    private String getOptimalZoomOnDescription()
+    {
+        return getIndexedDescription(TAG_OPTICAL_ZOOM_ON, "Off", "On");
+    }
+
+    @Nullable
+    private String getDigitalZoomOnDescription()
+    {
+        return getIndexedDescription(TAG_DIGITAL_ZOOM_ON, "Off", "On");
+    }
+
+    @Nullable
+    private String getLightSourceSpecialDescription()
+    {
+        return getIndexedDescription(TAG_LIGHT_SOURCE_SPECIAL, "Off", "On");
+    }
+
+    @Nullable
+    private String getResavedDescription()
+    {
+        return getIndexedDescription(TAG_RESAVED, "No", "Yes");
+    }
+
+    @Nullable
+    private String getSceneSelectDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_SELECT,
+            "Off", "Sport", "TV", "Night", "User 1", "User 2", "Lamp");
+    }
+
+    @Nullable
+    private String getSequenceShotIntervalDescription()
+    {
+        return getIndexedDescription(TAG_SEQUENCE_SHOT_INTERVAL,
+            "5 frames/sec", "10 frames/sec", "15 frames/sec", "20 frames/sec");
+    }
+
+    @Nullable
+    private String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE,
+            "Auto", "Force", "Disabled", "Red eye");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sanyo cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SanyoMakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_OFFSET = 0x00ff;
+
+    public static final int TAG_SANYO_THUMBNAIL = 0x0100;
+
+    public static final int TAG_SPECIAL_MODE = 0x0200;
+    public static final int TAG_SANYO_QUALITY = 0x0201;
+    public static final int TAG_MACRO = 0x0202;
+    public static final int TAG_DIGITAL_ZOOM = 0x0204;
+    public static final int TAG_SOFTWARE_VERSION = 0x0207;
+    public static final int TAG_PICT_INFO = 0x0208;
+    public static final int TAG_CAMERA_ID = 0x0209;
+    public static final int TAG_SEQUENTIAL_SHOT = 0x020e;
+    public static final int TAG_WIDE_RANGE = 0x020f;
+    public static final int TAG_COLOR_ADJUSTMENT_MODE = 0x0210;
+    public static final int TAG_QUICK_SHOT = 0x0213;
+    public static final int TAG_SELF_TIMER = 0x0214;
+    public static final int TAG_VOICE_MEMO = 0x0216;
+    public static final int TAG_RECORD_SHUTTER_RELEASE = 0x0217;
+    public static final int TAG_FLICKER_REDUCE = 0x0218;
+    public static final int TAG_OPTICAL_ZOOM_ON = 0x0219;
+    public static final int TAG_DIGITAL_ZOOM_ON = 0x021b;
+    public static final int TAG_LIGHT_SOURCE_SPECIAL = 0x021d;
+    public static final int TAG_RESAVED = 0x021e;
+    public static final int TAG_SCENE_SELECT = 0x021f;
+    public static final int TAG_MANUAL_FOCUS_DISTANCE_OR_FACE_INFO = 0x0223;
+    public static final int TAG_SEQUENCE_SHOT_INTERVAL = 0x0224;
+    public static final int TAG_FLASH_MODE = 0x0225;
+
+    public static final int TAG_PRINT_IM = 0x0e00;
+
+    public static final int TAG_DATA_DUMP = 0x0f00;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_OFFSET, "Makernote Offset");
+
+        _tagNameMap.put(TAG_SANYO_THUMBNAIL, "Sanyo Thumbnail");
+
+        _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode");
+        _tagNameMap.put(TAG_SANYO_QUALITY, "Sanyo Quality");
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_SOFTWARE_VERSION, "Software Version");
+        _tagNameMap.put(TAG_PICT_INFO, "Pict Info");
+        _tagNameMap.put(TAG_CAMERA_ID, "Camera ID");
+        _tagNameMap.put(TAG_SEQUENTIAL_SHOT, "Sequential Shot");
+        _tagNameMap.put(TAG_WIDE_RANGE, "Wide Range");
+        _tagNameMap.put(TAG_COLOR_ADJUSTMENT_MODE, "Color Adjustment Node");
+        _tagNameMap.put(TAG_QUICK_SHOT, "Quick Shot");
+        _tagNameMap.put(TAG_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_VOICE_MEMO, "Voice Memo");
+        _tagNameMap.put(TAG_RECORD_SHUTTER_RELEASE, "Record Shutter Release");
+        _tagNameMap.put(TAG_FLICKER_REDUCE, "Flicker Reduce");
+        _tagNameMap.put(TAG_OPTICAL_ZOOM_ON, "Optical Zoom On");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM_ON, "Digital Zoom On");
+        _tagNameMap.put(TAG_LIGHT_SOURCE_SPECIAL, "Light Source Special");
+        _tagNameMap.put(TAG_RESAVED, "Resaved");
+        _tagNameMap.put(TAG_SCENE_SELECT, "Scene Select");
+        _tagNameMap.put(TAG_MANUAL_FOCUS_DISTANCE_OR_FACE_INFO, "Manual Focus Distance or Face Info");
+        _tagNameMap.put(TAG_SEQUENCE_SHOT_INTERVAL, "Sequence Shot Interval");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+
+        _tagNameMap.put(TAG_PRINT_IM, "Print IM");
+
+        _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
+    }
+
+    public SanyoMakernoteDirectory()
+    {
+        this.setDescriptor(new SanyoMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sanyo Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SigmaMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SigmaMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SigmaMakernoteDescriptor extends TagDescriptor<SigmaMakernoteDirectory>
+{
+    public SigmaMakernoteDescriptor(@NotNull SigmaMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case TAG_METERING_MODE:
+                return getMeteringModeDescription();
+        }
+        return super.getDescription(tagType);
+    }
+
+    @Nullable
+    private String getMeteringModeDescription()
+    {
+        String value = _directory.getString(TAG_METERING_MODE);
+        if (value == null || value.length() == 0)
+            return null;
+        switch (value.charAt(0)) {
+            case '8': return "Multi Segment";
+            case 'A': return "Average";
+            case 'C': return "Center Weighted Average";
+            default:
+                return value;
+        }
+    }
+
+    @Nullable
+    private String getExposureModeDescription()
+    {
+        String value = _directory.getString(TAG_EXPOSURE_MODE);
+        if (value == null || value.length() == 0)
+            return null;
+        switch (value.charAt(0)) {
+            case 'A': return "Aperture Priority AE";
+            case 'M': return "Manual";
+            case 'P': return "Program AE";
+            case 'S': return "Shutter Speed Priority AE";
+            default:
+                return value;
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+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 https://drewnoakes.com
+ */
+public class SigmaMakernoteDirectory extends Directory
+{
+    public static final int TAG_SERIAL_NUMBER = 0x2;
+    public static final int TAG_DRIVE_MODE = 0x3;
+    public static final int TAG_RESOLUTION_MODE = 0x4;
+    public static final int TAG_AUTO_FOCUS_MODE = 0x5;
+    public static final int TAG_FOCUS_SETTING = 0x6;
+    public static final int TAG_WHITE_BALANCE = 0x7;
+    public static final int TAG_EXPOSURE_MODE = 0x8;
+    public static final int TAG_METERING_MODE = 0x9;
+    public static final int TAG_LENS_RANGE = 0xa;
+    public static final int TAG_COLOR_SPACE = 0xb;
+    public static final int TAG_EXPOSURE = 0xc;
+    public static final int TAG_CONTRAST = 0xd;
+    public static final int TAG_SHADOW = 0xe;
+    public static final int TAG_HIGHLIGHT = 0xf;
+    public static final int TAG_SATURATION = 0x10;
+    public static final int TAG_SHARPNESS = 0x11;
+    public static final int TAG_FILL_LIGHT = 0x12;
+    public static final int TAG_COLOR_ADJUSTMENT = 0x14;
+    public static final int TAG_ADJUSTMENT_MODE = 0x15;
+    public static final int TAG_QUALITY = 0x16;
+    public static final int TAG_FIRMWARE = 0x17;
+    public static final int TAG_SOFTWARE = 0x18;
+    public static final int TAG_AUTO_BRACKET = 0x19;
+
+    @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));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sigma Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,677 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SonyType1MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SonyType1MakernoteDirectory}.
+ * Thanks to David Carson for the initial version of this class.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1MakernoteDirectory>
+{
+    public SonyType1MakernoteDescriptor(@NotNull SonyType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_IMAGE_QUALITY:
+                return getImageQualityDescription();
+            case TAG_FLASH_EXPOSURE_COMP:
+                return getFlashExposureCompensationDescription();
+            case TAG_TELECONVERTER:
+                return getTeleconverterDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_COLOR_TEMPERATURE:
+                return getColorTemperatureDescription();
+            case TAG_SCENE_MODE:
+                return getSceneModeDescription();
+            case TAG_ZONE_MATCHING:
+                return getZoneMatchingDescription();
+            case TAG_DYNAMIC_RANGE_OPTIMISER:
+                return getDynamicRangeOptimizerDescription();
+            case 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 TAG_LENS_ID:
+//                return getLensIDDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_MACRO:
+                return getMacroDescription();
+            case TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case TAG_JPEG_QUALITY:
+                return getJpegQualityDescription();
+            case TAG_ANTI_BLUR:
+                return getAntiBlurDescription();
+            case TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE:
+                return getLongExposureNoiseReductionDescription();
+            case TAG_HIGH_ISO_NOISE_REDUCTION:
+                return getHighIsoNoiseReductionDescription();
+            case TAG_PICTURE_EFFECT:
+                return getPictureEffectDescription();
+            case TAG_SOFT_SKIN_EFFECT:
+                return getSoftSkinEffectDescription();
+            case TAG_VIGNETTING_CORRECTION:
+                return getVignettingCorrectionDescription();
+            case TAG_LATERAL_CHROMATIC_ABERRATION:
+                return getLateralChromaticAberrationDescription();
+            case TAG_DISTORTION_CORRECTION:
+                return getDistortionCorrectionDescription();
+            case TAG_AUTO_PORTRAIT_FRAMED:
+                return getAutoPortraitFramedDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_AF_POINT_SELECTED:
+                return getAFPointSelectedDescription();
+            case TAG_SONY_MODEL_ID:
+                return getSonyModelIdDescription();
+            case TAG_AF_MODE:
+                return getAFModeDescription();
+            case TAG_AF_ILLUMINATOR:
+                return getAFIlluminatorDescription();
+            case TAG_FLASH_LEVEL:
+                return getFlashLevelDescription();
+            case TAG_RELEASE_MODE:
+                return getReleaseModeDescription();
+            case TAG_SEQUENCE_NUMBER:
+                return getSequenceNumberDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getImageQualityDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_QUALITY,
+            "RAW",
+            "Super Fine",
+            "Fine",
+            "Standard",
+            "Economy",
+            "Extra Fine",
+            "RAW + JPEG",
+            "Compressed RAW",
+            "Compressed RAW + JPEG");
+    }
+
+    @Nullable
+    public String getFlashExposureCompensationDescription()
+    {
+        return getFormattedInt(TAG_FLASH_EXPOSURE_COMP, "%d EV");
+    }
+
+    @Nullable
+    public String getTeleconverterDescription()
+    {
+        Integer value = _directory.getInteger(TAG_TELECONVERTER);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "None";
+            case 0x48: return "Minolta/Sony AF 2x APO (D)";
+            case 0x50: return "Minolta AF 2x APO II";
+            case 0x60: return "Minolta AF 2x APO";
+            case 0x88: return "Minolta/Sony AF 1.4x APO (D)";
+            case 0x90: return "Minolta AF 1.4x APO II";
+            case 0xa0: return "Minolta AF 1.4x APO";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "Auto";
+            case 0x01: return "Color Temperature/Color Filter";
+            case 0x10: return "Daylight";
+            case 0x20: return "Cloudy";
+            case 0x30: return "Shade";
+            case 0x40: return "Tungsten";
+            case 0x50: return "Flash";
+            case 0x60: return "Fluorescent";
+            case 0x70: return "Custom";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorTemperatureDescription()
+    {
+        Integer value = _directory.getInteger(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 getZoneMatchingDescription()
+    {
+        return getIndexedDescription(TAG_ZONE_MATCHING,
+            "ISO Setting Used", "High Key", "Low Key");
+    }
+
+    @Nullable
+    public String getDynamicRangeOptimizerDescription()
+    {
+        Integer value = _directory.getInteger(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 3: return "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";
+            case 16: return "LV1";
+            case 17: return "LV2";
+            case 18: return "LV3";
+            case 19: return "LV4";
+            case 20: return "LV5";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        Integer value = _directory.getInteger(TAG_IMAGE_STABILISATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On";
+            default: return "N/A";
+        }
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(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 13: case 101: return "Clear";
+            case 14: case 102: return "Deep";
+            case 15: case 103: return "Light";
+            case 16: return "Autumn";
+            case 17: return "Sepia";
+            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(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(TAG_EXPOSURE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Program";
+            case 1: return "Portrait";
+            case 2: return "Beach";
+            case 3: return "Sports";
+            case 4: return "Snow";
+            case 5: return "Landscape";
+            case 6: return "Auto";
+            case 7: return "Aperture Priority";
+            case 8: return "Shutter Priority";
+            case 9: return "Night Scene / Twilight";
+            case 10: return "Hi-Speed Shutter";
+            case 11: return "Twilight Portrait";
+            case 12: return "Soft Snap/Portrait";
+            case 13: return "Fireworks";
+            case 14: return "Smile Shutter";
+            case 15: return "Manual";
+            case 18: return "High Sensitivity";
+            case 19: return "Macro";
+            case 20: return "Advanced Sports Shooting";
+            case 29: return "Underwater";
+            case 33: return "Food";
+            case 34: return "Panorama";
+            case 35: return "Handheld Night Shot";
+            case 36: return "Anti Motion Blur";
+            case 37: return "Pet";
+            case 38: return "Backlight Correction HDR";
+            case 39: return "Superior Auto";
+            case 40: return "Background Defocus";
+            case 41: return "Soft Skin";
+            case 42: return "3D Image";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getJpegQualityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_JPEG_QUALITY);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Normal";
+            case 1: return "Fine";
+            case 2: return "Extra Fine";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getAntiBlurDescription()
+    {
+        Integer value = _directory.getInteger(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(TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE);
+        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);
+        }
+    }
+
+    @Nullable
+    public String getHighIsoNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_HIGH_ISO_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On";
+            case 2: return "Normal";
+            case 3: return "High";
+            case 0x100: return "Auto";
+            case 0xffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getPictureEffectDescription()
+    {
+        Integer value = _directory.getInteger(TAG_PICTURE_EFFECT);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Toy Camera";
+            case 2: return "Pop Color";
+            case 3: return "Posterization";
+            case 4: return "Posterization B/W";
+            case 5: return "Retro Photo";
+            case 6: return "Soft High Key";
+            case 7: return "Partial Color (red)";
+            case 8: return "Partial Color (green)";
+            case 9: return "Partial Color (blue)";
+            case 10: return "Partial Color (yellow)";
+            case 13: return "High Contrast Monochrome";
+            case 16: return "Toy Camera (normal)";
+            case 17: return "Toy Camera (cool)";
+            case 18: return "Toy Camera (warm)";
+            case 19: return "Toy Camera (green)";
+            case 20: return "Toy Camera (magenta)";
+            case 32: return "Soft Focus (low)";
+            case 33: return "Soft Focus";
+            case 34: return "Soft Focus (high)";
+            case 48: return "Miniature (auto)";
+            case 49: return "Miniature (top)";
+            case 50: return "Miniature (middle horizontal)";
+            case 51: return "Miniature (bottom)";
+            case 52: return "Miniature (left)";
+            case 53: return "Miniature (middle vertical)";
+            case 54: return "Miniature (right)";
+            case 64: return "HDR Painting (low)";
+            case 65: return "HDR Painting";
+            case 66: return "HDR Painting (high)";
+            case 80: return "Rich-tone Monochrome";
+            case 97: return "Water Color";
+            case 98: return "Water Color 2";
+            case 112: return "Illustration (low)";
+            case 113: return "Illustration";
+            case 114: return "Illustration (high)";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getSoftSkinEffectDescription()
+    {
+        return getIndexedDescription(TAG_SOFT_SKIN_EFFECT, "Off", "Low", "Mid", "High");
+    }
+
+    @Nullable
+    public String getVignettingCorrectionDescription()
+    {
+        return getIndexedDescription(TAG_VIGNETTING_CORRECTION, "Off", null, "Auto");
+    }
+
+    @Nullable
+    public String getLateralChromaticAberrationDescription()
+    {
+        return getIndexedDescription(TAG_LATERAL_CHROMATIC_ABERRATION, "Off", null, "Auto");
+    }
+
+    @Nullable
+    public String getDistortionCorrectionDescription()
+    {
+        return getIndexedDescription(TAG_DISTORTION_CORRECTION, "Off", null, "Auto");
+    }
+
+    @Nullable
+    public String getAutoPortraitFramedDescription()
+    {
+        return getIndexedDescription(TAG_AUTO_PORTRAIT_FRAMED, "No", "Yes");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE,
+            "Manual", null, "AF-A", "AF-C", "AF-S", null, "DMF", "AF-D");
+    }
+
+    @Nullable
+    public String getAFPointSelectedDescription()
+    {
+        return getIndexedDescription(TAG_AF_POINT_SELECTED,
+            "Auto", // 0
+            "Center", // 1
+            "Top", // 2
+            "Upper-right", // 3
+            "Right", // 4
+            "Lower-right", // 5
+            "Bottom", // 6
+            "Lower-left", // 7
+            "Left", // 8
+            "Upper-left	  	", // 9
+            "Far Right", // 10
+            "Far Left", // 11
+            "Upper-middle", // 12
+            "Near Right", // 13
+            "Lower-middle", // 14
+            "Near Left", // 15
+            "Upper Far Right", // 16
+            "Lower Far Right", // 17
+            "Lower Far Left", // 18
+            "Upper Far Left" // 19
+        );
+    }
+
+    @Nullable
+    public String getSonyModelIdDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SONY_MODEL_ID);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 2: return "DSC-R1";
+            case 256: return "DSLR-A100";
+            case 257: return "DSLR-A900";
+            case 258: return "DSLR-A700";
+            case 259: return "DSLR-A200";
+            case 260: return "DSLR-A350";
+            case 261: return "DSLR-A300";
+            case 262: return "DSLR-A900 (APS-C mode)";
+            case 263: return "DSLR-A380/A390";
+            case 264: return "DSLR-A330";
+            case 265: return "DSLR-A230";
+            case 266: return "DSLR-A290";
+            case 269: return "DSLR-A850";
+            case 270: return "DSLR-A850 (APS-C mode)";
+            case 273: return "DSLR-A550";
+            case 274: return "DSLR-A500";
+            case 275: return "DSLR-A450";
+            case 278: return "NEX-5";
+            case 279: return "NEX-3";
+            case 280: return "SLT-A33";
+            case 281: return "SLT-A55V";
+            case 282: return "DSLR-A560";
+            case 283: return "DSLR-A580";
+            case 284: return "NEX-C3";
+            case 285: return "SLT-A35";
+            case 286: return "SLT-A65V";
+            case 287: return "SLT-A77V";
+            case 288: return "NEX-5N";
+            case 289: return "NEX-7";
+            case 290: return "NEX-VG20E";
+            case 291: return "SLT-A37";
+            case 292: return "SLT-A57";
+            case 293: return "NEX-F3";
+            case 294: return "SLT-A99V";
+            case 295: return "NEX-6";
+            case 296: return "NEX-5R";
+            case 297: return "DSC-RX100";
+            case 298: return "DSC-RX1";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(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";
+            case 18: return "Sweep Panorama";
+            case 19: return "Handheld Night Shot";
+            case 20: return "Anti Motion Blur";
+            case 21: return "Cont. Priority AE";
+            case 22: return "Auto+";
+            case 23: return "3D Sweep Panorama";
+            case 24: return "Superior Auto";
+            case 25: return "High Sensitivity";
+            case 26: return "Fireworks";
+            case 27: return "Food";
+            case 28: return "Pet";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAFModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_AF_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Default";
+            case 1: return "Multi";
+            case 2: return "Center";
+            case 3: return "Spot";
+            case 4: return "Flexible Spot";
+            case 6: return "Touch";
+            case 14: return "Manual Focus";
+            case 15: return "Face Detected";
+            case 0xffff: return "n/a";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAFIlluminatorDescription()
+    {
+        Integer value = _directory.getInteger(TAG_AF_ILLUMINATOR);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Auto";
+            case 0xffff: return "n/a";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashLevelDescription()
+    {
+        Integer value = _directory.getInteger(TAG_FLASH_LEVEL);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case -32768: return "Low";
+            case -3: return "-3/3";
+            case -2: return "-2/3";
+            case -1: return "-1/3";
+            case 0: return "Normal";
+            case 1: return "+1/3";
+            case 2: return "+2/3";
+            case 3: return "+3/3";
+            case 128: return "n/a";
+            case 32767: return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getReleaseModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_RELEASE_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Normal";
+            case 2: return "Continuous";
+            case 5: return "Exposure Bracketing";
+            case 6: return "White Balance Bracketing";
+            case 65535: return "n/a";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSequenceNumberDescription()
+    {
+        Integer value = _directory.getInteger(TAG_RELEASE_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Single";
+            case 65535: return "n/a";
+            default:
+                return value.toString();
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+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 https://drewnoakes.com
+ */
+public class SonyType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_CAMERA_INFO = 0x0010;
+    public static final int TAG_FOCUS_INFO = 0x0020;
+
+    public static final int TAG_IMAGE_QUALITY = 0x0102;
+    public static final int TAG_FLASH_EXPOSURE_COMP = 0x0104;
+    public static final int TAG_TELECONVERTER = 0x0105;
+
+    public static final int TAG_WHITE_BALANCE_FINE_TUNE = 0x0112;
+    public static final int TAG_CAMERA_SETTINGS = 0x0114;
+    public static final int TAG_WHITE_BALANCE = 0x0115;
+    public static final int TAG_EXTRA_INFO = 0x0116;
+
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    public static final int TAG_MULTI_BURST_MODE = 0x1000;
+    public static final int TAG_MULTI_BURST_IMAGE_WIDTH = 0x1001;
+    public static final int TAG_MULTI_BURST_IMAGE_HEIGHT = 0x1002;
+    public static final int TAG_PANORAMA = 0x1003;
+
+    public static final int TAG_PREVIEW_IMAGE = 0x2001;
+    public static final int TAG_RATING = 0x2002;
+    public static final int TAG_CONTRAST = 0x2004;
+    public static final int TAG_SATURATION = 0x2005;
+    public static final int TAG_SHARPNESS = 0x2006;
+    public static final int TAG_BRIGHTNESS = 0x2007;
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION = 0x2008;
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION = 0x2009;
+    public static final int TAG_HDR = 0x200a;
+    public static final int TAG_MULTI_FRAME_NOISE_REDUCTION = 0x200b;
+    public static final int TAG_PICTURE_EFFECT = 0x200e;
+    public static final int TAG_SOFT_SKIN_EFFECT = 0x200f;
+
+    public static final int TAG_VIGNETTING_CORRECTION = 0x2011;
+    public static final int TAG_LATERAL_CHROMATIC_ABERRATION = 0x2012;
+    public static final int TAG_DISTORTION_CORRECTION = 0x2013;
+    public static final int TAG_WB_SHIFT_AMBER_MAGENTA = 0x2014;
+    public static final int TAG_AUTO_PORTRAIT_FRAMED = 0x2016;
+    public static final int TAG_FOCUS_MODE = 0x201b;
+    public static final int TAG_AF_POINT_SELECTED = 0x201e;
+
+    public static final int TAG_SHOT_INFO = 0x3000;
+
+    public static final int TAG_FILE_FORMAT = 0xb000;
+    public static final int TAG_SONY_MODEL_ID = 0xb001;
+
+    public static final int TAG_COLOR_MODE_SETTING = 0xb020;
+    public static final int TAG_COLOR_TEMPERATURE = 0xb021;
+    public static final int TAG_COLOR_COMPENSATION_FILTER = 0xb022;
+    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_MAKERNOTE = 0xb028;
+    public static final int TAG_COLOR_MODE = 0xb029;
+    public static final int TAG_LENS_SPEC = 0xb02a;
+    public static final int TAG_FULL_IMAGE_SIZE = 0xb02b;
+    public static final int TAG_PREVIEW_IMAGE_SIZE = 0xb02c;
+
+    public static final int TAG_MACRO = 0xb040;
+    public static final int TAG_EXPOSURE_MODE = 0xb041;
+    public static final int TAG_FOCUS_MODE_2 = 0xb042;
+    public static final int TAG_AF_MODE = 0xb043;
+    public static final int TAG_AF_ILLUMINATOR = 0xb044;
+    public static final int TAG_JPEG_QUALITY = 0xb047;
+    public static final int TAG_FLASH_LEVEL = 0xb048;
+    public static final int TAG_RELEASE_MODE = 0xb049;
+    public static final int TAG_SEQUENCE_NUMBER = 0xb04a;
+    public static final int TAG_ANTI_BLUR = 0xb04b;
+    /**
+     * (FocusMode for RX100)
+     * 0 = Manual
+     * 2 = AF-S
+     * 3 = AF-C
+     * 5 = Semi-manual
+     * 6 = Direct Manual Focus
+     * (LongExposureNoiseReduction for other models)
+     * 0 = Off
+     * 1 = On
+     * 2 = On 2
+     * 65535 = n/a
+     */
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE = 0xb04e;
+    public static final int TAG_DYNAMIC_RANGE_OPTIMIZER = 0xb04f;
+
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION_2 = 0xb050;
+    public static final int TAG_INTELLIGENT_AUTO = 0xb052;
+    public static final int TAG_WHITE_BALANCE_2 = 0xb054;
+
+    public static final int TAG_NO_PRINT = 0xFFFF;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CAMERA_INFO, "Camera Info");
+        _tagNameMap.put(TAG_FOCUS_INFO, "Focus Info");
+
+        _tagNameMap.put(TAG_IMAGE_QUALITY, "Image Quality");
+        _tagNameMap.put(TAG_FLASH_EXPOSURE_COMP, "Flash Exposure Compensation");
+        _tagNameMap.put(TAG_TELECONVERTER, "Teleconverter Model");
+
+        _tagNameMap.put(TAG_WHITE_BALANCE_FINE_TUNE, "White Balance Fine Tune Value");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_EXTRA_INFO, "Extra Info");
+
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching Info");
+
+        _tagNameMap.put(TAG_MULTI_BURST_MODE, "Multi Burst Mode");
+        _tagNameMap.put(TAG_MULTI_BURST_IMAGE_WIDTH, "Multi Burst Image Width");
+        _tagNameMap.put(TAG_MULTI_BURST_IMAGE_HEIGHT, "Multi Burst Image Height");
+        _tagNameMap.put(TAG_PANORAMA, "Panorama");
+
+        _tagNameMap.put(TAG_PREVIEW_IMAGE, "Preview Image");
+        _tagNameMap.put(TAG_RATING, "Rating");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_BRIGHTNESS, "Brightness");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_HDR, "HDR");
+        _tagNameMap.put(TAG_MULTI_FRAME_NOISE_REDUCTION, "Multi Frame Noise Reduction");
+        _tagNameMap.put(TAG_PICTURE_EFFECT, "Picture Effect");
+        _tagNameMap.put(TAG_SOFT_SKIN_EFFECT, "Soft Skin Effect");
+
+        _tagNameMap.put(TAG_VIGNETTING_CORRECTION, "Vignetting Correction");
+        _tagNameMap.put(TAG_LATERAL_CHROMATIC_ABERRATION, "Lateral Chromatic Aberration");
+        _tagNameMap.put(TAG_DISTORTION_CORRECTION, "Distortion Correction");
+        _tagNameMap.put(TAG_WB_SHIFT_AMBER_MAGENTA, "WB Shift Amber/Magenta");
+        _tagNameMap.put(TAG_AUTO_PORTRAIT_FRAMED, "Auto Portrait Framing");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_AF_POINT_SELECTED, "AF Point Selected");
+
+        _tagNameMap.put(TAG_SHOT_INFO, "Shot Info");
+
+        _tagNameMap.put(TAG_FILE_FORMAT, "File Format");
+        _tagNameMap.put(TAG_SONY_MODEL_ID, "Sony Model ID");
+
+        _tagNameMap.put(TAG_COLOR_MODE_SETTING, "Color Mode Setting");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_COLOR_COMPENSATION_FILTER, "Color Compensation Filter");
+        _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_MAKERNOTE, "Minolta Makernote");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_LENS_SPEC, "Lens Spec");
+        _tagNameMap.put(TAG_FULL_IMAGE_SIZE, "Full Image Size");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_SIZE, "Preview Image Size");
+
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(TAG_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(TAG_AF_MODE, "AF Mode");
+        _tagNameMap.put(TAG_AF_ILLUMINATOR, "AF Illuminator");
+        _tagNameMap.put(TAG_JPEG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FLASH_LEVEL, "Flash Level");
+        _tagNameMap.put(TAG_RELEASE_MODE, "Release Mode");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(TAG_ANTI_BLUR, "Anti Blur");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_DYNAMIC_RANGE_OPTIMIZER, "Dynamic Range Optimizer");
+
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION_2, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_INTELLIGENT_AUTO, "Intelligent Auto");
+        _tagNameMap.put(TAG_WHITE_BALANCE_2, "White Balance 2");
+
+        _tagNameMap.put(TAG_NO_PRINT, "No Print");
+    }
+
+    public SonyType1MakernoteDirectory()
+    {
+        this.setDescriptor(new SonyType1MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sony Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 8132)
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SonyType6MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SonyType6MakernoteDescriptor extends TagDescriptor<SonyType6MakernoteDirectory>
+{
+    public SonyType6MakernoteDescriptor(@NotNull SonyType6MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_THUMB_VERSION:
+                return getMakernoteThumbVersionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getMakernoteThumbVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_THUMB_VERSION, 2);
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 8132)
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+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 https://drewnoakes.com
+ */
+public class SonyType6MakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_THUMB_OFFSET = 0x0513;
+    public static final int TAG_MAKERNOTE_THUMB_LENGTH = 0x0514;
+//    public static final int TAG_UNKNOWN_1 = 0x0515;
+    public static final int TAG_MAKERNOTE_THUMB_VERSION = 0x2000;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_THUMB_OFFSET, "Makernote Thumb Offset");
+        _tagNameMap.put(TAG_MAKERNOTE_THUMB_LENGTH, "Makernote Thumb Length");
+//        _tagNameMap.put(TAG_UNKNOWN_1, "Sony-6-0x0203");
+        _tagNameMap.put(TAG_MAKERNOTE_THUMB_VERSION, "Makernote Thumb Version");
+    }
+
+    public SonyType6MakernoteDirectory()
+    {
+        this.setDescriptor(new SonyType6MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sony Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/package.html
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/package.html	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/makernotes/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the modelling of manufacturer-specific makernotes.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/metadata/exif/package.html
===================================================================
--- /trunk/src/com/drew/metadata/exif/package.html	(revision 8132)
+++ /trunk/src/com/drew/metadata/exif/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/metadata/iptc/IptcDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.iptc;
@@ -27,9 +27,9 @@
 
 /**
- * Provides human-readable string representations of tag values stored in a <code>IptcDirectory</code>.
- * <p/>
+ * Provides human-readable string representations of tag values stored in a {@link IptcDirectory}.
+ * <p>
  * As the IPTC directory already stores values as strings, this class simply returns the tag's value.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class IptcDescriptor extends TagDescriptor<IptcDirectory>
@@ -40,4 +40,5 @@
     }
 
+    @Override
     @Nullable
     public String getDescription(int tagType)
Index: /trunk/src/com/drew/metadata/iptc/IptcDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.iptc;
@@ -32,5 +32,5 @@
  * Describes tags used by the International Press Telecommunications Council (IPTC) metadata format.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class IptcDirectory extends Directory
@@ -211,10 +211,12 @@
     }
 
+    @Override
     @NotNull
     public String getName()
     {
-        return "Iptc";
-    }
-
+        return "IPTC";
+    }
+
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
Index: /trunk/src/com/drew/metadata/iptc/IptcReader.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,24 +16,29 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.iptc;
 
-import com.drew.lang.BufferBoundsException;
-import com.drew.lang.BufferReader;
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataReader;
-
+
+import java.io.IOException;
+import java.util.Arrays;
 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
+ * Decodes IPTC binary data, populating a {@link Metadata} object with tag values in an {@link IptcDirectory}.
+ * <p>
+ * http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf
+ *
+ * @author Drew Noakes https://drewnoakes.com
  */
-public class IptcReader implements MetadataReader
+public class IptcReader implements JpegSegmentMetadataReader
 {
     // TODO consider breaking the IPTC section up into multiple directories and providing segregation of each IPTC directory
@@ -52,6 +57,25 @@
 */
 
-    /** 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)
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Arrays.asList(JpegSegmentType.APPD);
+    }
+
+    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    {
+        // Check whether the first byte resembles
+        return segmentBytes.length != 0 && segmentBytes[0] == 0x1c;
+    }
+
+    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
+    }
+
+    /**
+     * Performs the IPTC data extraction, adding found values to the specified instance of {@link Metadata}.
+     */
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length)
     {
         IptcDirectory directory = metadata.getOrCreateDirectory(IptcDirectory.class);
@@ -59,39 +83,30 @@
         int offset = 0;
 
-/*
-        // 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;
-        }
-*/
-
         // for each tag
-        while (offset < reader.getLength()) {
+        while (offset < length) {
 
             // identifies start of a tag
             short startByte;
             try {
-                startByte = reader.getUInt8(offset);
-            } catch (BufferBoundsException e) {
+                startByte = reader.getUInt8();
+                offset++;
+            } catch (IOException e) {
                 directory.addError("Unable to read starting byte of IPTC tag");
-                break;
+                return;
             }
 
             if (startByte != 0x1c) {
-                directory.addError("Invalid start to IPTC tag");
-                break;
+                // NOTE have seen images where there was one extra byte at the end, giving
+                // offset==length at this point, which is not worth logging as an error.
+                if (offset != length)
+                    directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x1c' but got '0x" + Integer.toHexString(startByte) + "'.");
+                return;
             }
 
             // we need at least five bytes left to read a tag
-            if (offset + 5 >= reader.getLength()) {
+            if (offset + 5 >= length) {
                 directory.addError("Too few bytes remain for a valid IPTC tag");
-                break;
-            }
-
-            offset++;
+                return;
+            }
 
             int directoryType;
@@ -99,42 +114,73 @@
             int tagByteCount;
             try {
-                directoryType = reader.getUInt8(offset++);
-                tagType = reader.getUInt8(offset++);
-                tagByteCount = reader.getUInt16(offset);
-                offset += 2;
-            } catch (BufferBoundsException e) {
+                directoryType = reader.getUInt8();
+                tagType = reader.getUInt8();
+                // TODO support Extended DataSet Tag (see 1.5(c), p14, IPTC-IIMV4.2.pdf)
+                tagByteCount = reader.getUInt16();
+                offset += 4;
+            } catch (IOException e) {
                 directory.addError("IPTC data segment ended mid-way through tag descriptor");
                 return;
             }
 
-            if (offset + tagByteCount > reader.getLength()) {
+            if (offset + tagByteCount > length) {
                 directory.addError("Data for tag extends beyond end of IPTC segment");
+                return;
+            }
+
+            try {
+                processTag(reader, directory, directoryType, tagType, tagByteCount);
+            } catch (IOException e) {
+                directory.addError("Error processing IPTC tag");
+                return;
+            }
+
+            offset += tagByteCount;
+        }
+    }
+
+    private void processTag(@NotNull SequentialReader reader, @NotNull Directory directory, int directoryType, int tagType, int tagByteCount) throws IOException
+    {
+        int tagIdentifier = tagType | (directoryType << 8);
+
+        // Some images have been seen that specify a zero byte tag, which cannot be of much use.
+        // We elect here to completely ignore the tag. The IPTC specification doesn't mention
+        // anything about the interpretation of this situation.
+        // https://raw.githubusercontent.com/wiki/drewnoakes/metadata-extractor/docs/IPTC-IIMV4.2.pdf
+        if (tagByteCount == 0) {
+            directory.setString(tagIdentifier, "");
+            return;
+        }
+
+        String string = null;
+
+        switch (tagIdentifier) {
+            case IptcDirectory.TAG_CODED_CHARACTER_SET:
+                byte[] bytes = reader.getBytes(tagByteCount);
+                String charset = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes);
+                if (charset == null) {
+                    // Unable to determine the charset, so fall through and treat tag as a regular string
+                    string = new String(bytes);
+                    break;
+                }
+                directory.setString(tagIdentifier, charset);
+                return;
+            case IptcDirectory.TAG_ENVELOPE_RECORD_VERSION:
+            case IptcDirectory.TAG_APPLICATION_RECORD_VERSION:
+            case IptcDirectory.TAG_FILE_VERSION:
+            case IptcDirectory.TAG_ARM_VERSION:
+            case IptcDirectory.TAG_PROGRAM_VERSION:
+                // short
+                if (tagByteCount >= 2) {
+                    int shortValue = reader.getUInt16();
+                    reader.skip(tagByteCount - 2);
+                    directory.setInt(tagIdentifier, shortValue);
+                    return;
+                }
                 break;
-            }
-
-            try {
-                processTag(reader, directory, directoryType, tagType, offset, tagByteCount);
-            } catch (BufferBoundsException e) {
-                directory.addError("Error processing IPTC tag");
-                break;
-            }
-
-            offset += 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_APPLICATION_RECORD_VERSION:
-                // short
-                int shortValue = reader.getUInt16(offset);
-                directory.setInt(tagIdentifier, shortValue);
-                return;
             case IptcDirectory.TAG_URGENCY:
                 // byte
-                directory.setInt(tagIdentifier, reader.getUInt8(offset));
+                directory.setInt(tagIdentifier, reader.getUInt8());
+                reader.skip(tagByteCount - 1);
                 return;
             case IptcDirectory.TAG_RELEASE_DATE:
@@ -142,15 +188,17 @@
                 // Date object
                 if (tagByteCount >= 8) {
-                    String dateStr = reader.getString(offset, tagByteCount);
+                    string = reader.getString(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));
+                        int year = Integer.parseInt(string.substring(0, 4));
+                        int month = Integer.parseInt(string.substring(4, 6)) - 1;
+                        int day = Integer.parseInt(string.substring(6, 8));
                         Date date = new java.util.GregorianCalendar(year, month, day).getTime();
                         directory.setDate(tagIdentifier, date);
                         return;
                     } catch (NumberFormatException e) {
-                        // fall through and we'll store whatever was there as a String
+                        // fall through and we'll process the 'string' value below
                     }
+                } else {
+                    reader.skip(tagByteCount);
                 }
             case IptcDirectory.TAG_RELEASE_TIME:
@@ -162,9 +210,14 @@
 
         // If we haven't returned yet, treat it as a string
-        String str;
-        if (tagByteCount < 1) {
-            str = "";
-        } else {
-            str = reader.getString(offset, tagByteCount, System.getProperty("file.encoding")); // "ISO-8859-1"
+        // NOTE that there's a chance we've already loaded the value as a string above, but failed to parse the value
+        if (string == null) {
+            String encoding = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET);
+            if (encoding != null) {
+                string = reader.getString(tagByteCount, encoding);
+            } else {
+                byte[] bytes = reader.getBytes(tagByteCount);
+                encoding = Iso2022Converter.guessEncoding(bytes);
+                string = encoding != null ? new String(bytes, encoding) : new String(bytes);
+            }
         }
 
@@ -179,8 +232,8 @@
                 System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length);
             }
-            newStrings[newStrings.length - 1] = str;
+            newStrings[newStrings.length - 1] = string;
             directory.setStringArray(tagIdentifier, newStrings);
         } else {
-            directory.setString(tagIdentifier, str);
+            directory.setString(tagIdentifier, string);
         }
     }
Index: /trunk/src/com/drew/metadata/iptc/Iso2022Converter.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 8132)
@@ -0,0 +1,83 @@
+package com.drew.metadata.iptc;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+
+public final class Iso2022Converter
+{
+    private static final String ISO_8859_1 = "ISO-8859-1";
+    private static final String UTF_8 = "UTF-8";
+
+    private static final byte LATIN_CAPITAL_A = 0x41;
+    private static final int DOT = 0xe280a2;
+    private static final byte LATIN_CAPITAL_G = 0x47;
+    private static final byte PERCENT_SIGN = 0x25;
+    private static final byte ESC = 0x1B;
+
+    /**
+     * Converts the given ISO2022 char set to a Java charset name.
+     *
+     * @param bytes string data encoded using ISO2022
+     * @return the Java charset name as a string, or <code>null</code> if the conversion was not possible
+     */
+    @Nullable
+    public static String convertISO2022CharsetToJavaCharset(@NotNull final byte[] bytes)
+    {
+        if (bytes.length > 2 && bytes[0] == ESC && bytes[1] == PERCENT_SIGN && bytes[2] == LATIN_CAPITAL_G)
+            return UTF_8;
+
+        if (bytes.length > 3 && bytes[0] == ESC && (bytes[3] & 0xFF | ((bytes[2] & 0xFF) << 8) | ((bytes[1] & 0xFF) << 16)) == DOT && bytes[4] == LATIN_CAPITAL_A)
+            return ISO_8859_1;
+
+        return null;
+    }
+
+    /**
+     * Attempts to guess the encoding of a string provided as a byte array.
+     * <p/>
+     * Encodings trialled are, in order:
+     * <ul>
+     *     <li>UTF-8</li>
+     *     <li><code>System.getProperty("file.encoding")</code></li>
+     *     <li>ISO-8859-1</li>
+     * </ul>
+     * <p/>
+     * Its only purpose is to guess the encoding if and only if iptc tag coded character set is not set. If the
+     * encoding is not UTF-8, the tag should be set. Otherwise it is bad practice. This method tries to
+     * workaround this issue since some metadata manipulating tools do not prevent such bad practice.
+     * <p/>
+     * About the reliability of this method: The check if some bytes are UTF-8 or not has a very high reliability.
+     * The two other checks are less reliable.
+     *
+     * @param bytes some text as bytes
+     * @return the name of the encoding or null if none could be guessed
+     */
+    @Nullable
+    static String guessEncoding(@NotNull final byte[] bytes)
+    {
+        String[] encodings = { UTF_8, System.getProperty("file.encoding"), ISO_8859_1 };
+
+        for (String encoding : encodings)
+        {
+            CharsetDecoder cs = Charset.forName(encoding).newDecoder();
+
+            try {
+                cs.decode(ByteBuffer.wrap(bytes));
+                return encoding;
+            } catch (CharacterCodingException e) {
+                // fall through...
+            }
+        }
+
+        // No encodings succeeded. Return null.
+        return null;
+    }
+
+    private Iso2022Converter()
+    {}
+}
Index: /trunk/src/com/drew/metadata/iptc/package.html
===================================================================
--- /trunk/src/com/drew/metadata/iptc/package.html	(revision 8132)
+++ /trunk/src/com/drew/metadata/iptc/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of IPTC metadata.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
@@ -26,7 +26,7 @@
 
 /**
- * Provides human-readable string representations of tag values stored in a <code>JpegCommentDirectory</code>.
+ * Provides human-readable string representations of tag values stored in a {@link JpegCommentDirectory}.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class JpegCommentDescriptor extends TagDescriptor<JpegCommentDirectory>
@@ -40,5 +40,5 @@
     public String getJpegCommentDescription()
     {
-        return _directory.getString(JpegCommentDirectory.TAG_JPEG_COMMENT);
+        return _directory.getString(JpegCommentDirectory.TAG_COMMENT);
     }
 }
Index: /trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
@@ -29,5 +29,5 @@
  * Describes tags used by a JPEG file comment.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class JpegCommentDirectory extends Directory
@@ -37,5 +37,5 @@
      * consistency with other directory types.
      */
-    public static final int TAG_JPEG_COMMENT = 0;
+    public static final int TAG_COMMENT = 0;
 
     @NotNull
@@ -43,5 +43,5 @@
 
     static {
-        _tagNameMap.put(TAG_JPEG_COMMENT, "Jpeg Comment");
+        _tagNameMap.put(TAG_COMMENT, "JPEG Comment");
     }
 
@@ -51,4 +51,5 @@
     }
 
+    @Override
     @NotNull
     public String getName()
@@ -57,4 +58,5 @@
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
Index: /trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,36 +16,42 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.lang.BufferBoundsException;
-import com.drew.lang.BufferReader;
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataReader;
+
+import java.util.Arrays;
 
 /**
- * Decodes the comment stored within Jpeg files, populating a <code>Metadata</code> object with tag values in a
- * <code>JpegCommentDirectory</code>.
+ * Decodes the comment stored within JPEG files, populating a {@link Metadata} object with tag values in a
+ * {@link JpegCommentDirectory}.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
-public class JpegCommentReader implements MetadataReader
+public class JpegCommentReader implements JpegSegmentMetadataReader
 {
-    /**
-     * Performs the Jpeg data extraction, adding found values to the specified
-     * instance of <code>Metadata</code>.
-     */
-    public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Arrays.asList(JpegSegmentType.COM);
+    }
+
+    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    {
+        // The entire contents of the byte[] is the comment. There's nothing here to discriminate upon.
+        return true;
+    }
+
+    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
         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");
-        }
+        // The entire contents of the directory are the comment
+        directory.setString(JpegCommentDirectory.TAG_COMMENT, new String(segmentBytes));
     }
 }
Index: /trunk/src/com/drew/metadata/jpeg/JpegComponent.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
@@ -26,8 +26,8 @@
 
 /**
- * Stores information about a Jpeg image component such as the component id, horiz/vert sampling factor and
+ * 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
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class JpegComponent implements Serializable
Index: /trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
@@ -29,5 +29,5 @@
  * Thanks to Darrell Silver (www.darrellsilver.com) for the initial version of this class.
  *
- * @author Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class JpegDescriptor extends TagDescriptor<JpegDirectory>
@@ -38,4 +38,5 @@
     }
 
+    @Override
     @Nullable
     public String getDescription(int tagType)
@@ -43,19 +44,19 @@
         switch (tagType)
         {
-            case JpegDirectory.TAG_JPEG_COMPRESSION_TYPE:
+            case JpegDirectory.TAG_COMPRESSION_TYPE:
                 return getImageCompressionTypeDescription();
-            case JpegDirectory.TAG_JPEG_COMPONENT_DATA_1:
+            case JpegDirectory.TAG_COMPONENT_DATA_1:
                 return getComponentDataDescription(0);
-            case JpegDirectory.TAG_JPEG_COMPONENT_DATA_2:
+            case JpegDirectory.TAG_COMPONENT_DATA_2:
                 return getComponentDataDescription(1);
-            case JpegDirectory.TAG_JPEG_COMPONENT_DATA_3:
+            case JpegDirectory.TAG_COMPONENT_DATA_3:
                 return getComponentDataDescription(2);
-            case JpegDirectory.TAG_JPEG_COMPONENT_DATA_4:
+            case JpegDirectory.TAG_COMPONENT_DATA_4:
                 return getComponentDataDescription(3);
-            case JpegDirectory.TAG_JPEG_DATA_PRECISION:
+            case JpegDirectory.TAG_DATA_PRECISION:
                 return getDataPrecisionDescription();
-            case JpegDirectory.TAG_JPEG_IMAGE_HEIGHT:
+            case JpegDirectory.TAG_IMAGE_HEIGHT:
                 return getImageHeightDescription();
-            case JpegDirectory.TAG_JPEG_IMAGE_WIDTH:
+            case JpegDirectory.TAG_IMAGE_WIDTH:
                 return getImageWidthDescription();
             default:
@@ -67,5 +68,5 @@
     public String getImageCompressionTypeDescription()
     {
-        Integer value = _directory.getInteger(JpegDirectory.TAG_JPEG_COMPRESSION_TYPE);
+        Integer value = _directory.getInteger(JpegDirectory.TAG_COMPRESSION_TYPE);
         if (value==null)
             return null;
@@ -93,5 +94,5 @@
     public String getImageWidthDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
+        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_WIDTH);
         if (value==null)
             return null;
@@ -102,5 +103,5 @@
     public String getImageHeightDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
+        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_HEIGHT);
         if (value==null)
             return null;
@@ -111,5 +112,5 @@
     public String getDataPrecisionDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_JPEG_DATA_PRECISION);
+        final String value = _directory.getString(JpegDirectory.TAG_DATA_PRECISION);
         if (value==null)
             return null;
@@ -125,14 +126,7 @@
             return null;
 
-        StringBuilder sb = new StringBuilder();
-        sb.append(value.getComponentName());
-        sb.append(" component: Quantization table ");
-        sb.append(value.getQuantizationTableNumber());
-        sb.append(", Sampling factors ");
-        sb.append(value.getHorizontalSamplingFactor());
-        sb.append(" horiz/");
-        sb.append(value.getVerticalSamplingFactor());
-        sb.append(" vert");
-        return sb.toString();
+        return value.getComponentName() + " component: Quantization table " + value.getQuantizationTableNumber()
+            + ", Sampling factors " + value.getHorizontalSamplingFactor()
+            + " horiz/" + value.getVerticalSamplingFactor() + " vert";
     }
 }
Index: /trunk/src/com/drew/metadata/jpeg/JpegDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,6 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
@@ -29,17 +29,17 @@
 
 /**
- * Directory of tags and values for the SOF0 Jpeg segment.  This segment holds basic metadata about the image.
+ * 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 http://drewnoakes.com
+ * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes https://drewnoakes.com
  */
 public class JpegDirectory extends Directory
 {
-    public static final int TAG_JPEG_COMPRESSION_TYPE = -3;
+    public static final int TAG_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;
+    public static final int TAG_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;
+    public static final int TAG_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;
+    public static final int TAG_IMAGE_WIDTH = 3;
     /**
      * Usually 1 = grey scaled, 3 = color YcbCr or YIQ, 4 = color CMYK
@@ -48,19 +48,19 @@
      * sampling factors (1byte) (bit 0-3 vertical., 4-7 horizontal.),
      * quantization table number (1 byte).
-     * <p/>
+     * <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 static final int TAG_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_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_1 = 6;
+    /** the second of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_2 = 7;
+    /** the third of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_3 = 8;
+    /** the fourth of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_4 = 9;
 
     @NotNull
@@ -68,13 +68,13 @@
 
     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");
+        _tagNameMap.put(TAG_COMPRESSION_TYPE, "Compression Type");
+        _tagNameMap.put(TAG_DATA_PRECISION, "Data Precision");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_NUMBER_OF_COMPONENTS, "Number of Components");
+        _tagNameMap.put(TAG_COMPONENT_DATA_1, "Component 1");
+        _tagNameMap.put(TAG_COMPONENT_DATA_2, "Component 2");
+        _tagNameMap.put(TAG_COMPONENT_DATA_3, "Component 3");
+        _tagNameMap.put(TAG_COMPONENT_DATA_4, "Component 4");
     }
 
@@ -84,10 +84,12 @@
     }
 
+    @Override
     @NotNull
     public String getName()
     {
-        return "Jpeg";
+        return "JPEG";
     }
 
+    @Override
     @NotNull
     protected HashMap<Integer, String> getTagNameMap()
@@ -104,5 +106,5 @@
     public JpegComponent getComponent(int componentNumber)
     {
-        int tagType = JpegDirectory.TAG_JPEG_COMPONENT_DATA_1 + componentNumber;
+        int tagType = JpegDirectory.TAG_COMPONENT_DATA_1 + componentNumber;
         return (JpegComponent)getObject(tagType);
     }
@@ -110,15 +112,15 @@
     public int getImageWidth() throws MetadataException
     {
-        return getInt(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
+        return getInt(JpegDirectory.TAG_IMAGE_WIDTH);
     }
 
     public int getImageHeight() throws MetadataException
     {
-        return getInt(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
+        return getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
     }
 
     public int getNumberOfComponents() throws MetadataException
     {
-        return getInt(JpegDirectory.TAG_JPEG_NUMBER_OF_COMPONENTS);
+        return getInt(JpegDirectory.TAG_NUMBER_OF_COMPONENTS);
     }
 }
Index: /trunk/src/com/drew/metadata/jpeg/JpegReader.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 8131)
+++ /trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 8132)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2012 Drew Noakes
+ * Copyright 2002-2015 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,46 +16,77 @@
  * More information about this project is available at:
  *
- *    http://drewnoakes.com/code/exif/
- *    http://code.google.com/p/metadata-extractor/
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.lang.BufferBoundsException;
-import com.drew.lang.BufferReader;
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataReader;
+
+import java.io.IOException;
+import java.util.Arrays;
 
 /**
- * Decodes Jpeg SOF0 data, populating a <code>Metadata</code> object with tag values in a <code>JpegDirectory</code>.
+ * Decodes JPEG SOFn data, populating a {@link Metadata} object with tag values in a {@link JpegDirectory}.
  *
- * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes http://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Darrell Silver http://www.darrellsilver.com
  */
-public class JpegReader implements MetadataReader
+public class JpegReader implements JpegSegmentMetadataReader
 {
-    /**
-     * Performs the Jpeg data extraction, adding found values to the specified
-     * instance of <code>Metadata</code>.
-     */
-    public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
     {
+        // NOTE that some SOFn values do not exist
+        return Arrays.asList(
+            JpegSegmentType.SOF0,
+            JpegSegmentType.SOF1,
+            JpegSegmentType.SOF2,
+            JpegSegmentType.SOF3,
+//            JpegSegmentType.SOF4,
+            JpegSegmentType.SOF5,
+            JpegSegmentType.SOF6,
+            JpegSegmentType.SOF7,
+            JpegSegmentType.SOF8,
+            JpegSegmentType.SOF9,
+            JpegSegmentType.SOF10,
+            JpegSegmentType.SOF11,
+//            JpegSegmentType.SOF12,
+            JpegSegmentType.SOF13,
+            JpegSegmentType.SOF14,
+            JpegSegmentType.SOF15
+        );
+    }
+
+    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    {
+        return true;
+    }
+
+    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        if (metadata.containsDirectory(JpegDirectory.class)) {
+            // If this directory is already present, discontinue this operation.
+            // We only store metadata for the *first* matching SOFn segment.
+            return;
+        }
+
         JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class);
 
+        // The value of TAG_COMPRESSION_TYPE is determined by the segment type found
+        directory.setInt(JpegDirectory.TAG_COMPRESSION_TYPE, segmentType.byteValue - JpegSegmentType.SOF0.byteValue);
+
+        SequentialReader reader = new SequentialByteArrayReader(segmentBytes);
+
         try {
-            // data precision
-            int dataPrecision = reader.getUInt8(JpegDirectory.TAG_JPEG_DATA_PRECISION);
-            directory.setInt(JpegDirectory.TAG_JPEG_DATA_PRECISION, dataPrecision);
-
-            // process height
-            int height = reader.getUInt16(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
-            directory.setInt(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT, height);
-
-            // process width
-            int width = reader.getUInt16(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
-            directory.setInt(JpegDirectory.TAG_JPEG_IMAGE_WIDTH, width);
-
-            // number of components
-            int numberOfComponents = reader.getUInt8(JpegDirectory.TAG_JPEG_NUMBER_OF_COMPONENTS);
-            directory.setInt(JpegDirectory.TAG_JPEG_NUMBER_OF_COMPONENTS, numberOfComponents);
+            directory.setInt(JpegDirectory.TAG_DATA_PRECISION, reader.getUInt8());
+            directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
+            directory.setInt(JpegDirectory.TAG_IMAGE_WIDTH, reader.getUInt16());
+            short componentCount = reader.getUInt8();
+            directory.setInt(JpegDirectory.TAG_NUMBER_OF_COMPONENTS, componentCount);
 
             // for each component, there are three bytes of data:
@@ -63,14 +94,13 @@
             // 2 - Sampling factors: bit 0-3 vertical, 4-7 horizontal
             // 3 - Quantization table number
-            int offset = 6;
-            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);
+            for (int i = 0; i < (int)componentCount; i++) {
+                final int componentId = reader.getUInt8();
+                final int samplingFactorByte = reader.getUInt8();
+                final int quantizationTableNumber = reader.getUInt8();
+                final JpegComponent component = new JpegComponent(componentId, samplingFactorByte, quantizationTableNumber);
+                directory.setObject(JpegDirectory.TAG_COMPONENT_DATA_1 + i, component);
             }
 
-        } catch (BufferBoundsException ex) {
+        } catch (IOException ex) {
             directory.addError(ex.getMessage());
         }
Index: /trunk/src/com/drew/metadata/jpeg/package.html
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/package.html	(revision 8132)
+++ /trunk/src/com/drew/metadata/jpeg/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of JPEG file format metadata.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/metadata/package.html
===================================================================
--- /trunk/src/com/drew/metadata/package.html	(revision 8132)
+++ /trunk/src/com/drew/metadata/package.html	(revision 8132)
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Provides classes for generic modelling of metadata directories and tags.  Contains base types for metadata processing abstraction.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
Index: /trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
===================================================================
--- /trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 8132)
+++ /trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 8132)
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2002-2015 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:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.tiff;
+
+import com.drew.imaging.tiff.TiffHandler;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+
+import java.util.Stack;
+
+/**
+ * Adapter between the {@link TiffHandler} interface and the {@link Metadata}/{@link Directory} object model.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class DirectoryTiffHandler implements TiffHandler
+{
+    private final Stack<Directory> _directoryStack = new Stack<Directory>();
+
+    protected Directory _currentDirectory;
+    protected final Metadata _metadata;
+
+    protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectory)
+    {
+        _metadata = metadata;
+        _currentDirectory = _metadata.getOrCreateDirectory(initialDirectory);
+    }
+
+    public void endingIFD()
+    {
+        _currentDirectory = _directoryStack.empty() ? null : _directoryStack.pop();
+    }
+
+    protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
+    {
+        assert(directoryClass != _currentDirectory.getClass());
+        _directoryStack.push(_currentDirectory);
+        _currentDirectory = _metadata.getOrCreateDirectory(directoryClass);
+    }
+
+    public void warn(@NotNull String message)
+    {
+        _currentDirectory.addError(message);
+    }
+
+    public void error(@NotNull String message)
+    {
+        _currentDirectory.addError(message);
+    }
+
+    public void setByteArray(int tagId, @NotNull byte[] bytes)
+    {
+        _currentDirectory.setByteArray(tagId, bytes);
+    }
+
+    public void setString(int tagId, @NotNull String string)
+    {
+        _currentDirectory.setString(tagId, string);
+    }
+
+    public void setRational(int tagId, @NotNull Rational rational)
+    {
+        _currentDirectory.setRational(tagId, rational);
+    }
+
+    public void setRationalArray(int tagId, @NotNull Rational[] array)
+    {
+        _currentDirectory.setRationalArray(tagId, array);
+    }
+
+    public void setFloat(int tagId, float float32)
+    {
+        _currentDirectory.setFloat(tagId, float32);
+    }
+
+    public void setFloatArray(int tagId, @NotNull float[] array)
+    {
+        _currentDirectory.setFloatArray(tagId, array);
+    }
+
+    public void setDouble(int tagId, double double64)
+    {
+        _currentDirectory.setDouble(tagId, double64);
+    }
+
+    public void setDoubleArray(int tagId, @NotNull double[] array)
+    {
+        _currentDirectory.setDoubleArray(tagId, array);
+    }
+
+    public void setInt8s(int tagId, byte int8s)
+    {
+        // NOTE Directory stores all integral types as int32s, except for int32u and long
+        _currentDirectory.setInt(tagId, int8s);
+    }
+
+    public void setInt8sArray(int tagId, @NotNull byte[] array)
+    {
+        // NOTE Directory stores all integral types as int32s, except for int32u and long
+        _currentDirectory.setByteArray(tagId, array);
+    }
+
+    public void setInt8u(int tagId, short int8u)
+    {
+        // NOTE Directory stores all integral types as int32s, except for int32u and long
+        _currentDirectory.setInt(tagId, int8u);
+    }
+
+    public void setInt8uArray(int tagId, @NotNull short[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+
+    public void setInt16s(int tagId, int int16s)
+    {
+        // TODO create and use a proper setter for int16u?
+        _currentDirectory.setInt(tagId, int16s);
+    }
+
+    public void setInt16sArray(int tagId, @NotNull short[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+
+    public void setInt16u(int tagId, int int16u)
+    {
+        // TODO create and use a proper setter for
+        _currentDirectory.setInt(tagId, int16u);
+    }
+
+    public void setInt16uArray(int tagId, @NotNull int[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+
+    public void setInt32s(int tagId, int int32s)
+    {
+        _currentDirectory.setInt(tagId, int32s);
+    }
+
+    public void setInt32sArray(int tagId, @NotNull int[] array)
+    {
+        _currentDirectory.setIntArray(tagId, array);
+    }
+
+    public void setInt32u(int tagId, long int32u)
+    {
+        _currentDirectory.setLong(tagId, int32u);
+    }
+
+    public void setInt32uArray(int tagId, @NotNull long[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+}
Index: /trunk/src/com/drew/metadata/tiff/package.html
===================================================================
--- /trunk/src/com/drew/metadata/tiff/package.html	(revision 8132)
+++ /trunk/src/com/drew/metadata/tiff/package.html	(revision 8132)
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright 2002-2015 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:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of TIFF file metadata.
+
+<!-- Put @see and @since tags down here. -->
+@since 2.7.0
+
+</body>
+</html>
Index: /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java	(revision 8131)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java	(revision 8132)
@@ -627,6 +627,6 @@
 
         try {
-            double speed = dirGps.getDouble(GpsDirectory.TAG_GPS_SPEED);
-            String speedRef = dirGps.getString(GpsDirectory.TAG_GPS_SPEED_REF);
+            double speed = dirGps.getDouble(GpsDirectory.TAG_SPEED);
+            String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
             if (speedRef != null) {
                 if (speedRef.equalsIgnoreCase("M")) {
@@ -645,6 +645,6 @@
 
         try {
-            double ele = dirGps.getDouble(GpsDirectory.TAG_GPS_ALTITUDE);
-            int d = dirGps.getInt(GpsDirectory.TAG_GPS_ALTITUDE_REF);
+            double ele = dirGps.getDouble(GpsDirectory.TAG_ALTITUDE);
+            int d = dirGps.getInt(GpsDirectory.TAG_ALTITUDE_REF);
             if (d == 1) {
                 ele *= -1;
@@ -679,5 +679,5 @@
         // 2) GPS_DATE_STAMP not set -> use EXIF date or set to default
         // 3) GPS_TIME_STAMP and GPS_DATE_STAMP are set
-        int[] timeStampComps = dirGps.getIntArray(GpsDirectory.TAG_GPS_TIME_STAMP);
+        int[] timeStampComps = dirGps.getIntArray(GpsDirectory.TAG_TIME_STAMP);
         if (timeStampComps != null) {
             int gpsHour = timeStampComps[0];
@@ -688,5 +688,5 @@
             // We have the time. Next step is to check if the GPS date stamp is set.
             // dirGps.getString() always succeeds, but the return value might be null.
-            String dateStampStr = dirGps.getString(GpsDirectory.TAG_GPS_DATE_STAMP);
+            String dateStampStr = dirGps.getString(GpsDirectory.TAG_DATE_STAMP);
             if (dateStampStr != null && dateStampStr.matches("^\\d+:\\d+:\\d+$")) {
                 String[] dateStampComps = dateStampStr.split(":");
Index: /trunk/src/org/openstreetmap/josm/tools/ExifReader.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/ExifReader.java	(revision 8131)
+++ /trunk/src/org/openstreetmap/josm/tools/ExifReader.java	(revision 8132)
@@ -2,4 +2,5 @@
 package org.openstreetmap.josm.tools;
 
+import java.awt.geom.AffineTransform;
 import java.io.File;
 import java.io.IOException;
@@ -21,5 +22,4 @@
 import com.drew.metadata.exif.ExifSubIFDDirectory;
 import com.drew.metadata.exif.GpsDirectory;
-import java.awt.geom.AffineTransform;
 
 /**
@@ -125,6 +125,6 @@
     public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
         if (dirGps != null) {
-            double lat = readAxis(dirGps, GpsDirectory.TAG_GPS_LATITUDE, GpsDirectory.TAG_GPS_LATITUDE_REF, 'S');
-            double lon = readAxis(dirGps, GpsDirectory.TAG_GPS_LONGITUDE, GpsDirectory.TAG_GPS_LONGITUDE_REF, 'W');
+            double lat = readAxis(dirGps, GpsDirectory.TAG_LATITUDE, GpsDirectory.TAG_LATITUDE_REF, 'S');
+            double lon = readAxis(dirGps, GpsDirectory.TAG_LONGITUDE, GpsDirectory.TAG_LONGITUDE_REF, 'W');
             return new LatLon(lat, lon);
         }
@@ -159,5 +159,5 @@
     public static Double readDirection(GpsDirectory dirGps) {
         if (dirGps != null) {
-            Rational direction = dirGps.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION);
+            Rational direction = dirGps.getRational(GpsDirectory.TAG_IMG_DIRECTION);
             if (direction != null) {
                 return direction.doubleValue();
