Index: trunk/src/com/drew/imaging/ImageProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 10861)
+++ trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/PhotographicConversions.java
===================================================================
--- trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 10861)
+++ trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 10861)
+++ trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -39,6 +39,8 @@
 import com.drew.metadata.iptc.IptcReader;
 //import com.drew.metadata.jfif.JfifReader;
+//import com.drew.metadata.jfxx.JfxxReader;
 import com.drew.metadata.jpeg.JpegCommentReader;
 import com.drew.metadata.jpeg.JpegReader;
+//import com.drew.metadata.photoshop.DuckyReader;
 //import com.drew.metadata.photoshop.PhotoshopReader;
 //import com.drew.metadata.xmp.XmpReader;
@@ -55,8 +57,10 @@
             new JpegCommentReader(),
             //new JfifReader(),
+            //new JfxxReader(),
             new ExifReader(),
             //new XmpReader(),
             //new IccReader(),
             //new PhotoshopReader(),
+            //new DuckyReader(),
             new IptcReader()//,
             //new AdobeJpegReader()
Index: trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 10861)
+++ trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 10861)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -52,5 +52,4 @@
      * @param segmentBytes the byte array holding data for the segment being added
      */
-    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     public void addSegment(byte segmentType, @NotNull byte[] segmentBytes)
     {
@@ -207,5 +206,4 @@
      * @param occurrence  the zero-based index of the segment occurrence to remove.
      */
-    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     public void removeSegmentOccurrence(@NotNull JpegSegmentType segmentType, int occurrence)
     {
@@ -220,5 +218,4 @@
      * @param occurrence  the zero-based index of the segment occurrence to remove.
      */
-    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     public void removeSegmentOccurrence(byte segmentType, int occurrence)
     {
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 10861)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -43,4 +43,9 @@
 public class JpegSegmentReader
 {
+    /**
+     * The 0xFF byte that signals the start of a segment.
+     */
+    private static final byte SEGMENT_IDENTIFIER = (byte) 0xFF;
+
     /**
      * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet).
@@ -112,17 +117,12 @@
             // by a 0xFF and then a byte not equal to 0x00 or 0xFF.
 
-            final short segmentIdentifier = reader.getUInt8();
+            byte segmentIdentifier = reader.getInt8();
+            byte segmentType = reader.getInt8();
 
-            // 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");
+            // Read until we have a 0xFF byte followed by a byte that is not 0xFF or 0x00
+            while (segmentIdentifier != SEGMENT_IDENTIFIER || segmentType == SEGMENT_IDENTIFIER || segmentType == 0) {
+            	segmentIdentifier = segmentType;
+            	segmentType = reader.getInt8();
+            }
 
             if (segmentType == SEGMENT_SOS) {
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 10861)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,15 +30,20 @@
  * An enumeration of the known segment types found in JPEG files.
  *
+ * <ul>
+ *     <li>http://www.ozhiker.com/electronics/pjmt/jpeg_info/app_segments.html</li>
+ *     <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html</li>
+ * </ul>
+ *
  * @author Drew Noakes https://drewnoakes.com
  */
 public enum JpegSegmentType
 {
-    /** APP0 JPEG segment identifier -- JFIF data (also JFXX apparently). */
+    /** APP0 JPEG segment identifier. Commonly contains JFIF, JFXX. */
     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 JPEG segment identifier. Commonly contains Exif. XMP data is also kept in here, though usually in a second instance. */
     APP1((byte)0xE1, true),
 
-    /** APP2 JPEG segment identifier. */
+    /** APP2 JPEG segment identifier. Commonly contains ICC. */
     APP2((byte)0xE2, true),
 
@@ -64,5 +69,5 @@
     APP9((byte)0xE9, true),
 
-    /** APPA (App10) JPEG segment identifier -- can hold Unicode comments. */
+    /** APPA (App10) JPEG segment identifier. Can contain Unicode comments, though {@link JpegSegmentType#COM} is more commonly used for comments. */
     APPA((byte)0xEA, true),
 
@@ -73,8 +78,8 @@
     APPC((byte)0xEC, true),
 
-    /** APPD (App13) JPEG segment identifier -- IPTC data in here. */
+    /** APPD (App13) JPEG segment identifier. Commonly contains IPTC, Photoshop data. */
     APPD((byte)0xED, true),
 
-    /** APPE (App14) JPEG segment identifier. */
+    /** APPE (App14) JPEG segment identifier. Commonly contains Adobe data. */
     APPE((byte)0xEE, true),
 
Index: trunk/src/com/drew/imaging/jpeg/package.html
===================================================================
--- trunk/src/com/drew/imaging/jpeg/package.html	(revision 10861)
+++ trunk/src/com/drew/imaging/jpeg/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/package.html
===================================================================
--- trunk/src/com/drew/imaging/package.html	(revision 10861)
+++ trunk/src/com/drew/imaging/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/tiff/TiffDataFormat.java
===================================================================
--- trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 10861)
+++ trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/tiff/TiffHandler.java
===================================================================
--- trunk/src/com/drew/imaging/tiff/TiffHandler.java	(revision 10861)
+++ trunk/src/com/drew/imaging/tiff/TiffHandler.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,4 +24,5 @@
 import com.drew.lang.Rational;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 
 import java.io.IOException;
@@ -46,5 +47,5 @@
     void setTiffMarker(int marker) throws TiffProcessingException;
 
-    boolean isTagIfdPointer(int tagType);
+    boolean tryEnterSubIfd(int tagId);
     boolean hasFollowerIfd();
 
@@ -52,4 +53,7 @@
 
     void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset);
+
+    @Nullable
+    Long tryCustomProcessFormat(int tagId, int formatCode, long componentCount);
 
     boolean customProcessTag(int tagOffset,
Index: trunk/src/com/drew/imaging/tiff/TiffProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 10861)
+++ trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/tiff/TiffReader.java
===================================================================
--- trunk/src/com/drew/imaging/tiff/TiffReader.java	(revision 10861)
+++ trunk/src/com/drew/imaging/tiff/TiffReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -110,4 +110,5 @@
                                   final int tiffHeaderOffset) throws IOException
     {
+        Boolean resetByteOrder = null;
         try {
             // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
@@ -126,4 +127,14 @@
             // First two bytes in the IFD are the number of tags in this directory
             int dirTagCount = reader.getUInt16(ifdOffset);
+
+            // Some software modifies the byte order of the file, but misses some IFDs (such as makernotes).
+            // The entire test image repository doesn't contain a single IFD with more than 255 entries.
+            // Here we detect switched bytes that suggest this problem, and temporarily swap the byte order.
+            // This was discussed in GitHub issue #136.
+            if (dirTagCount > 0xFF && (dirTagCount & 0xFF) == 0) {
+                resetByteOrder = reader.isMotorolaByteOrder();
+                dirTagCount >>= 8;
+                reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder());
+            }
 
             int dirLength = (2 + (12 * dirTagCount) + 4);
@@ -147,29 +158,30 @@
                 final TiffDataFormat format = TiffDataFormat.fromTiffFormatCode(formatCode);
 
+                // 4 bytes dictate the number of components in this tag's data
+                final long componentCount = reader.getUInt32(tagOffset + 4);
+
+                final long byteCount;
                 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;
+                    Long byteCountOverride = handler.tryCustomProcessFormat(tagId, formatCode, componentCount);
+                    if (byteCountOverride == 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(String.format("Invalid TIFF tag format code %d for tag 0x%04X", formatCode, tagId));
+                        // 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;
                     }
-                    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;
+                    byteCount = byteCountOverride;
+                } else {
+                    byteCount = componentCount * format.getComponentSizeBytes();
+                }
+
+                final long tagValueOffset;
                 if (byteCount > 4) {
                     // If it's bigger than 4 bytes, the dir entry contains an offset.
-                    final int offsetVal = reader.getInt32(tagOffset + 8);
+                    final long offsetVal = reader.getUInt32(tagOffset + 8);
                     if (offsetVal + byteCount > reader.getLength()) {
                         // Bogus pointer offset and / or byteCount value
@@ -195,14 +207,20 @@
                 }
 
-                //
-                // 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);
+                // Some tags point to one or more additional IFDs to process
+                boolean isIfdPointer = false;
+                if (byteCount == 4 * componentCount) {
+                    for (int i = 0; i < componentCount; i++) {
+                        if (handler.tryEnterSubIfd(tagId)) {
+                            isIfdPointer = true;
+                            int subDirOffset = tiffHeaderOffset + reader.getInt32((int) (tagValueOffset + i * 4));
+                            processIfd(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset);
+                        }
                     }
+                }
+
+                // If it wasn't an IFD pointer, allow custom tag processing to occur
+                if (!isIfdPointer && !handler.customProcessTag((int) tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, (int) byteCount)) {
+                    // If no custom processing occurred, process the tag in the standard fashion
+                    processTag(handler, tagId, (int) tagValueOffset, (int) componentCount, formatCode, reader);
                 }
             }
@@ -229,4 +247,6 @@
         } finally {
             handler.endingIFD();
+            if (resetByteOrder != null)
+                reader.setMotorolaByteOrder(resetByteOrder);
         }
     }
@@ -350,5 +370,5 @@
                 break;
             default:
-                handler.error(String.format("Unknown format code %d for tag %d", formatCode, tagId));
+                handler.error(String.format("Invalid TIFF tag format code %d for tag 0x%04X", formatCode, tagId));
         }
     }
Index: trunk/src/com/drew/imaging/tiff/package.html
===================================================================
--- trunk/src/com/drew/imaging/tiff/package.html	(revision 10861)
+++ trunk/src/com/drew/imaging/tiff/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- trunk/src/com/drew/lang/BufferBoundsException.java	(revision 10861)
+++ trunk/src/com/drew/lang/BufferBoundsException.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/ByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/ByteArrayReader.java	(revision 10861)
+++ trunk/src/com/drew/lang/ByteArrayReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,7 @@
 package com.drew.lang;
 
+import java.io.IOException;
+
 import com.drew.lang.annotations.NotNull;
-
-import java.io.IOException;
 
 /**
@@ -40,6 +40,4 @@
     private final byte[] _buffer;
 
-    @SuppressWarnings({ "ConstantConditions" })
-    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
     public ByteArrayReader(@NotNull byte[] buffer)
     {
@@ -74,5 +72,5 @@
         return bytesRequested >= 0
             && index >= 0
-            && (long)index + (long)bytesRequested - 1L < (long)_buffer.length;
+            && (long)index + (long)bytesRequested - 1L < _buffer.length;
     }
 
Index: trunk/src/com/drew/lang/CompoundException.java
===================================================================
--- trunk/src/com/drew/lang/CompoundException.java	(revision 10861)
+++ trunk/src/com/drew/lang/CompoundException.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/DateUtil.java
===================================================================
--- trunk/src/com/drew/lang/DateUtil.java	(revision 10862)
+++ trunk/src/com/drew/lang/DateUtil.java	(revision 10862)
@@ -0,0 +1,32 @@
+package com.drew.lang;
+
+/**
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class DateUtil
+{
+    private static int[] _daysInMonth365 = new int[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+    public static boolean isValidDate(int year, int month, int day)
+    {
+        if (year < 1 || year > 9999 || month < 0 || month > 11)
+            return false;
+
+        int daysInMonth = _daysInMonth365[month];
+        if (month == 1)
+        {
+            boolean isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+            if (isLeapYear)
+                daysInMonth++;
+        }
+
+        return day >= 1 && day <= daysInMonth;
+    }
+
+    public static boolean isValidTime(int hours, int minutes, int seconds)
+    {
+        return hours >= 0 && hours < 24
+            && minutes >= 0 && minutes < 60
+            && seconds >= 0 && seconds < 60;
+    }
+}
Index: trunk/src/com/drew/lang/GeoLocation.java
===================================================================
--- trunk/src/com/drew/lang/GeoLocation.java	(revision 10861)
+++ trunk/src/com/drew/lang/GeoLocation.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -84,5 +84,5 @@
         double[] dms = decimalToDegreesMinutesSeconds(decimal);
         DecimalFormat format = new DecimalFormat("0.##");
-        return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
+        return String.format("%s\u00B0 %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 10861)
+++ 	(revision )
@@ -1,43 +1,0 @@
-/*
- * 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 java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * An implementation of OutputSteam that ignores write requests by doing nothing.  This class may be useful in tests.
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public class NullOutputStream extends OutputStream
-{
-    public NullOutputStream()
-    {
-        super();
-    }
-
-    @Override
-    public void write(int b) throws IOException
-    {
-        // do nothing
-    }
-}
Index: trunk/src/com/drew/lang/RandomAccessReader.java
===================================================================
--- trunk/src/com/drew/lang/RandomAccessReader.java	(revision 10861)
+++ trunk/src/com/drew/lang/RandomAccessReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -53,5 +53,5 @@
      * @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 IllegalArgumentException <code>index</code> is 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
Index: trunk/src/com/drew/lang/Rational.java
===================================================================
--- trunk/src/com/drew/lang/Rational.java	(revision 10861)
+++ trunk/src/com/drew/lang/Rational.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/SequentialByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 10861)
+++ trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -42,5 +42,4 @@
     }
 
-    @SuppressWarnings("ConstantConditions")
     public SequentialByteArrayReader(@NotNull byte[] bytes, int baseIndex)
     {
Index: trunk/src/com/drew/lang/SequentialReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialReader.java	(revision 10861)
+++ trunk/src/com/drew/lang/SequentialReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/StreamReader.java
===================================================================
--- trunk/src/com/drew/lang/StreamReader.java	(revision 10861)
+++ trunk/src/com/drew/lang/StreamReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,5 +37,4 @@
     private final InputStream _stream;
 
-    @SuppressWarnings("ConstantConditions")
     public StreamReader(@NotNull InputStream stream)
     {
Index: trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- trunk/src/com/drew/lang/StringUtil.java	(revision 10861)
+++ trunk/src/com/drew/lang/StringUtil.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- trunk/src/com/drew/lang/annotations/NotNull.java	(revision 10861)
+++ trunk/src/com/drew/lang/annotations/NotNull.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/annotations/Nullable.java
===================================================================
--- trunk/src/com/drew/lang/annotations/Nullable.java	(revision 10861)
+++ trunk/src/com/drew/lang/annotations/Nullable.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/annotations/SuppressWarnings.java
===================================================================
--- trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 10861)
+++ 	(revision )
@@ -1,42 +1,0 @@
-/*
- * Copyright 2002-2011 Andreas Ziermann
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang.annotations;
-
-/**
- * Used to suppress specific code analysis warnings produced by the Findbugs tool.
- *
- * @author Andreas Ziermann
- */
-public @interface SuppressWarnings
-{
-    /**
-     * The name of the warning to be suppressed.
-     * @return The name of the warning to be suppressed.
-     */
-    @NotNull String value();
-
-    /**
-     * An explanation of why it is valid to suppress the warning in a particular situation/context.
-     * @return An explanation of why it is valid to suppress the warning in a particular situation/context.
-     */
-    @NotNull String justification();
-}
Index: trunk/src/com/drew/lang/annotations/package.html
===================================================================
--- trunk/src/com/drew/lang/annotations/package.html	(revision 10861)
+++ trunk/src/com/drew/lang/annotations/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/package.html
===================================================================
--- trunk/src/com/drew/lang/package.html	(revision 10861)
+++ trunk/src/com/drew/lang/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Age.java
===================================================================
--- trunk/src/com/drew/metadata/Age.java	(revision 10861)
+++ trunk/src/com/drew/metadata/Age.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Directory.java
===================================================================
--- trunk/src/com/drew/metadata/Directory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/Directory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,12 +24,14 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
-import com.drew.lang.annotations.SuppressWarnings;
 
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
 import java.text.DateFormat;
+import java.text.DecimalFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
@@ -41,4 +43,6 @@
 public abstract class Directory
 {
+    private static final DecimalFormat _floatFormat = new DecimalFormat("0.###");
+
     /** Map of values hashed by type identifiers. */
     @NotNull
@@ -59,4 +63,7 @@
     protected TagDescriptor _descriptor;
 
+    @Nullable
+    private Directory _parent;
+
 // ABSTRACT METHODS
 
@@ -171,4 +178,15 @@
     {
         return _errorList.size();
+    }
+
+    @Nullable
+    public Directory getParent()
+    {
+        return _parent;
+    }
+
+    public void setParent(@NotNull Directory parent)
+    {
+        _parent = parent;
     }
 
@@ -701,5 +719,4 @@
     /** Returns the specified tag's value as a boolean.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
     @Nullable
-    @SuppressWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "keep API interface consistent")
     public Boolean getBooleanObject(int tagType)
     {
@@ -725,10 +742,10 @@
      * <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.
+     * the GMT {@link TimeZone}.  If the {@link TimeZone} is known, call the overload that accepts one as an argument.
      */
     @Nullable
     public java.util.Date getDate(int tagType)
     {
-        return getDate(tagType, null);
+        return getDate(tagType, null, null);
     }
 
@@ -738,19 +755,39 @@
      * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
      * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).  Note that this parameter
-     * is only considered if the underlying value is a string and parsing occurs, otherwise it has no effect.
+     * is only considered if the underlying value is a string and it has no time zone information, otherwise it has no effect.
      */
     @Nullable
     public java.util.Date getDate(int tagType, @Nullable TimeZone timeZone)
     {
-        Object o = getObject(tagType);
-
-        if (o == null)
-            return null;
+        return getDate(tagType, null, timeZone);
+    }
+
+    /**
+     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
+     * <p>
+     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
+     * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).  Note that this parameter
+     * is only considered if the underlying value is a string and it has no time zone information, otherwise it has no effect.
+     * In addition, the {@code subsecond} parameter, which specifies the number of digits after the decimal point in the seconds,
+     * is set to the returned Date. This parameter is only considered if the underlying value is a string and is has
+     * no subsecond information, otherwise it has no effect.
+     *
+     * @param tagType the tag identifier
+     * @param subsecond the subsecond value for the Date
+     * @param timeZone the time zone to use
+     * @return a Date representing the time value
+     */
+    @Nullable
+    public java.util.Date getDate(int tagType, @Nullable String subsecond, @Nullable TimeZone timeZone)
+    {
+        Object o = getObject(tagType);
 
         if (o instanceof java.util.Date)
             return (java.util.Date)o;
 
+        java.util.Date date = null;
+
         if (o instanceof String) {
-            // This seems to cover all known Exif date strings
+            // This seems to cover all known Exif and Xmp date strings
             // Note that "    :  :     :  :  " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
             String datePatterns[] = {
@@ -760,6 +797,28 @@
                     "yyyy-MM-dd HH:mm",
                     "yyyy.MM.dd HH:mm:ss",
-                    "yyyy.MM.dd HH:mm" };
+                    "yyyy.MM.dd HH:mm",
+                    "yyyy-MM-dd'T'HH:mm:ss",
+                    "yyyy-MM-dd'T'HH:mm",
+                    "yyyy-MM-dd",
+                    "yyyy-MM",
+                    "yyyy" };
             String dateString = (String)o;
+
+            // if the date string has subsecond information, it supersedes the subsecond parameter
+            Pattern subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)");
+            Matcher subsecondMatcher = subsecondPattern.matcher(dateString);
+            if (subsecondMatcher.find()) {
+                subsecond = subsecondMatcher.group(2).substring(1);
+                dateString = subsecondMatcher.replaceAll("$1");
+            }
+
+            // if the date string has time zone information, it supersedes the timeZone parameter
+            Pattern timeZonePattern = Pattern.compile("(Z|[+-]\\d\\d:\\d\\d)$");
+            Matcher timeZoneMatcher = timeZonePattern.matcher(dateString);
+            if (timeZoneMatcher.find()) {
+                timeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replaceAll("Z", ""));
+                dateString = timeZoneMatcher.replaceAll("");
+            }
+
             for (String datePattern : datePatterns) {
                 try {
@@ -770,5 +829,6 @@
                         parser.setTimeZone(TimeZone.getTimeZone("GMT")); // don't interpret zone time
 
-                    return parser.parse(dateString);
+                    date = parser.parse(dateString);
+                    break;
                 } catch (ParseException ex) {
                     // simply try the next pattern
@@ -776,5 +836,23 @@
             }
         }
-        return null;
+
+        if (date == null)
+            return null;
+
+        if (subsecond == null)
+            return date;
+
+        try {
+            int millisecond = (int) (Double.parseDouble("." + subsecond) * 1000);
+            if (millisecond >= 0 && millisecond < 1000) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(date);
+                calendar.set(Calendar.MILLISECOND, millisecond);
+                return calendar.getTime();
+            }
+            return date;
+        } catch (NumberFormatException e) {
+            return date;
+        }
     }
 
@@ -835,34 +913,62 @@
             int arrayLength = Array.getLength(o);
             final Class<?> componentType = o.getClass().getComponentType();
-            boolean isObjectArray = Object.class.isAssignableFrom(componentType);
-            boolean isFloatArray = componentType.getName().equals("float");
-            boolean isDoubleArray = componentType.getName().equals("double");
-            boolean isIntArray = componentType.getName().equals("int");
-            boolean isLongArray = componentType.getName().equals("long");
-            boolean isByteArray = componentType.getName().equals("byte");
-            boolean isShortArray = componentType.getName().equals("short");
+
             StringBuilder string = new StringBuilder();
-            for (int i = 0; i < arrayLength; i++) {
-                if (i != 0)
-                    string.append(' ');
-                if (isObjectArray)
+
+            if (Object.class.isAssignableFrom(componentType)) {
+                // object array
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.get(o, i).toString());
-                else if (isIntArray)
+                }
+            } else if (componentType.getName().equals("int")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.getInt(o, i));
-                else if (isShortArray)
+                }
+            } else if (componentType.getName().equals("short")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.getShort(o, i));
-                else if (isLongArray)
+                }
+            } else if (componentType.getName().equals("long")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.getLong(o, i));
-                else if (isFloatArray)
-                    string.append(Array.getFloat(o, i));
-                else if (isDoubleArray)
-                    string.append(Array.getDouble(o, i));
-                else if (isByteArray)
-                    string.append(Array.getByte(o, i));
-                else
-                    addError("Unexpected array component type: " + componentType.getName());
-            }
+                }
+            } else if (componentType.getName().equals("float")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
+                    string.append(_floatFormat.format(Array.getFloat(o, i)));
+                }
+            } else if (componentType.getName().equals("double")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
+                    string.append(_floatFormat.format(Array.getDouble(o, i)));
+                }
+            } else if (componentType.getName().equals("byte")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
+                    string.append(Array.getByte(o, i) & 0xff);
+                }
+            } else {
+                addError("Unexpected array component type: " + componentType.getName());
+            }
+
             return string.toString();
         }
+
+        if (o instanceof Double)
+            return _floatFormat.format(((Double)o).doubleValue());
+
+        if (o instanceof Float)
+            return _floatFormat.format(((Float)o).floatValue());
 
         // Note that several cameras leave trailing spaces (Olympus, Nikon) but this library is intended to show
Index: trunk/src/com/drew/metadata/Face.java
===================================================================
--- trunk/src/com/drew/metadata/Face.java	(revision 10861)
+++ trunk/src/com/drew/metadata/Face.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Metadata.java
===================================================================
--- trunk/src/com/drew/metadata/Metadata.java	(revision 10861)
+++ trunk/src/com/drew/metadata/Metadata.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -36,6 +36,9 @@
 public final class Metadata
 {
+    /**
+     * The list of {@link Directory} instances in this container, in the order they were added.
+     */
     @NotNull
-    private final Map<Class<? extends Directory>,Collection<Directory>> _directoryListByClass = new HashMap<Class<? extends Directory>, Collection<Directory>>();
+    private final List<Directory> _directories = new ArrayList<>();
 
     /**
@@ -47,11 +50,18 @@
     public Iterable<Directory> getDirectories()
     {
-        return new DirectoryIterable(_directoryListByClass);
+        return _directories;
     }
 
     @Nullable
+    @SuppressWarnings("unchecked")
     public <T extends Directory> Collection<T> getDirectoriesOfType(Class<T> type)
     {
-        return (Collection<T>)_directoryListByClass.get(type);
+        List<T> directories = new ArrayList<>();
+        for (Directory dir : _directories) {
+            if (type.isAssignableFrom(dir.getClass())) {
+                directories.add((T)dir);
+            }
+        }
+        return directories;
     }
 
@@ -63,8 +73,5 @@
     public int getDirectoryCount()
     {
-        int count = 0;
-        for (Map.Entry<Class<? extends Directory>,Collection<Directory>> pair : _directoryListByClass.entrySet())
-            count += pair.getValue().size();
-        return count;
+        return _directories.size();
     }
 
@@ -76,5 +83,5 @@
     public <T extends Directory> void addDirectory(@NotNull T directory)
     {
-        getOrCreateDirectoryList(directory.getClass()).add(directory);
+        _directories.add(directory);
     }
 
@@ -91,13 +98,9 @@
     public <T extends Directory> T getFirstDirectoryOfType(@NotNull Class<T> type)
     {
-        // We suppress the warning here as the code asserts a map signature of Class<T>,T.
-        // So after get(Class<T>) it is for sure the result is from type T.
-
-        Collection<Directory> list = getDirectoryList(type);
-
-        if (list == null || list.isEmpty())
-            return null;
-
-        return (T)list.iterator().next();
+        for (Directory dir : _directories) {
+            if (type.isAssignableFrom(dir.getClass()))
+                return (T)dir;
+        }
+        return null;
     }
 
@@ -110,6 +113,9 @@
     public boolean containsDirectoryOfType(Class<? extends Directory> type)
     {
-        Collection<Directory> list = getDirectoryList(type);
-        return list != null && !list.isEmpty();
+        for (Directory dir : _directories) {
+            if (type.isAssignableFrom(dir.getClass()))
+                return true;
+        }
+        return false;
     }
 
@@ -139,72 +145,3 @@
                 : "directories");
     }
-
-    @Nullable
-    private <T extends Directory> Collection<Directory> getDirectoryList(@NotNull Class<T> type)
-    {
-        return _directoryListByClass.get(type);
-    }
-
-    @NotNull
-    private <T extends Directory> Collection<Directory> getOrCreateDirectoryList(@NotNull Class<T> type)
-    {
-        Collection<Directory> collection = getDirectoryList(type);
-        if (collection != null)
-            return collection;
-        collection = new ArrayList<Directory>();
-        _directoryListByClass.put(type, collection);
-        return collection;
-    }
-
-    private static class DirectoryIterable implements Iterable<Directory>
-    {
-        private final Map<Class<? extends Directory>, Collection<Directory>> _map;
-
-        public DirectoryIterable(Map<Class<? extends Directory>, Collection<Directory>> map)
-        {
-            _map = map;
-        }
-
-        public Iterator<Directory> iterator()
-        {
-            return new DirectoryIterator(_map);
-        }
-
-        private static class DirectoryIterator implements Iterator<Directory>
-        {
-            @NotNull
-            private final Iterator<Map.Entry<Class<? extends Directory>, Collection<Directory>>> _mapIterator;
-            @Nullable
-            private Iterator<Directory> _listIterator;
-
-            public DirectoryIterator(Map<Class<? extends Directory>, Collection<Directory>> map)
-            {
-                _mapIterator = map.entrySet().iterator();
-
-                if (_mapIterator.hasNext())
-                    _listIterator = _mapIterator.next().getValue().iterator();
-            }
-
-            public boolean hasNext()
-            {
-                return _listIterator != null && (_listIterator.hasNext() || _mapIterator.hasNext());
-            }
-
-            public Directory next()
-            {
-                if (_listIterator == null || (!_listIterator.hasNext() && !_mapIterator.hasNext()))
-                    throw new NoSuchElementException();
-
-                while (!_listIterator.hasNext())
-                    _listIterator = _mapIterator.next().getValue().iterator();
-
-                return _listIterator.next();
-            }
-
-            public void remove()
-            {
-                throw new UnsupportedOperationException();
-            }
-        }
-    }
 }
Index: trunk/src/com/drew/metadata/MetadataException.java
===================================================================
--- trunk/src/com/drew/metadata/MetadataException.java	(revision 10861)
+++ trunk/src/com/drew/metadata/MetadataException.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/MetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/MetadataReader.java	(revision 10861)
+++ trunk/src/com/drew/metadata/MetadataReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Tag.java
===================================================================
--- trunk/src/com/drew/metadata/Tag.java	(revision 10861)
+++ trunk/src/com/drew/metadata/Tag.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -54,5 +54,5 @@
     /**
      * Gets the tag type in hex notation as a String with padded leading
-     * zeroes if necessary (i.e. <code>0x100E</code>).
+     * zeroes if necessary (i.e. <code>0x100e</code>).
      *
      * @return the tag type as a string in hexadecimal notation
@@ -61,7 +61,5 @@
     public String getTagTypeHex()
     {
-        String hex = Integer.toHexString(_tagType);
-        while (hex.length() < 4) hex = "0" + hex;
-        return "0x" + hex;
+        return String.format("0x%04x", _tagType);
     }
 
@@ -117,5 +115,5 @@
 
     /**
-     * A basic representation of the tag's type and value.  EG: <code>[FNumber] F2.8</code>.
+     * A basic representation of the tag's type and value.  EG: <code>[Exif IFD0] FNumber - f/2.8</code>.
      *
      * @return the tag's type and value
Index: trunk/src/com/drew/metadata/TagDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/TagDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/TagDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,4 +28,7 @@
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -71,7 +74,14 @@
             final int length = Array.getLength(object);
             if (length > 16) {
-                final String componentTypeName = object.getClass().getComponentType().getName();
-                return String.format("[%d %s%s]", length, componentTypeName, length == 1 ? "" : "s");
+                return String.format("[%d %s]", length, length == 1 ? "value" : "values");
             }
+        }
+
+        if (object instanceof Date)
+        {
+            // Produce a date string having a format that includes the offset in form "+00:00"
+            return new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy")
+                .format((Date) object)
+                .replaceAll("([0-9]{2} [^ ]+)$", ":$1");
         }
 
@@ -272,3 +282,66 @@
         }
     }
+
+    @Nullable
+    protected String getRationalOrDoubleString(int tagType)
+    {
+        Rational rational = _directory.getRational(tagType);
+        if (rational != null)
+            return rational.toSimpleString(true);
+
+        Double d = _directory.getDoubleObject(tagType);
+        if (d != null)
+        {
+            DecimalFormat format = new DecimalFormat("0.###");
+            return format.format(d);
+        }
+
+        return null;
+    }
+
+    @Nullable
+    protected static String getFStopDescription(double fStop)
+    {
+        DecimalFormat format = new DecimalFormat("0.0");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return "f/" + format.format(fStop);
+    }
+
+    @Nullable
+    protected static String getFocalLengthDescription(double mm)
+    {
+        DecimalFormat format = new DecimalFormat("0.#");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return format.format(mm) + " mm";
+    }
+
+    @Nullable
+    protected String getLensSpecificationDescription(int tag)
+    {
+        Rational[] values = _directory.getRationalArray(tag);
+
+        if (values == null || values.length != 4 || (values[0].doubleValue() == 0 && values[2].doubleValue() == 0))
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        if (values[0].equals(values[1]))
+            sb.append(values[0].toSimpleString(true)).append("mm");
+        else
+            sb.append(values[0].toSimpleString(true)).append('-').append(values[1].toSimpleString(true)).append("mm");
+
+        if (values[2].doubleValue() != 0) {
+            sb.append(' ');
+
+            DecimalFormat format = new DecimalFormat("0.0");
+            format.setRoundingMode(RoundingMode.HALF_UP);
+
+            if (values[2].equals(values[3]))
+                sb.append(getFStopDescription(values[2].doubleValue()));
+            else
+                sb.append("f/").append(format.format(values[2].doubleValue())).append('-').append(format.format(values[3].doubleValue()));
+        }
+
+        return sb.toString();
+    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,4 +30,5 @@
 
 import java.io.UnsupportedEncodingException;
+import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.util.HashMap;
@@ -51,6 +52,4 @@
     @NotNull
     private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
-    @NotNull
-    private static final java.text.DecimalFormat SimpleDecimalFormatterWithPrecision = new DecimalFormat("0.0");
 
     // Note for the potential addition of brightness presentation in eV:
@@ -206,4 +205,6 @@
             case TAG_JPEG_PROC:
                 return getJpegProcDescription();
+            case TAG_LENS_SPECIFICATION:
+                return getLensSpecificationDescription();
             default:
                 return super.getDescription(tagType);
@@ -509,4 +510,10 @@
 
     @Nullable
+    public String getLensSpecificationDescription()
+    {
+        return getLensSpecificationDescription(TAG_LENS_SPECIFICATION);
+    }
+
+    @Nullable
     public String getSharpnessDescription()
     {
@@ -568,6 +575,6 @@
             ? null
             : value == 0
-            ? "Unknown"
-            : SimpleDecimalFormatter.format(value) + "mm";
+                ? "Unknown"
+                : getFocalLengthDescription(value);
     }
 
@@ -579,6 +586,6 @@
             ? null
             : value.getNumerator() == 0
-            ? "Digital zoom not used."
-            : SimpleDecimalFormatter.format(value.doubleValue());
+                ? "Digital zoom not used"
+                : SimpleDecimalFormatter.format(value.doubleValue());
     }
 
@@ -711,5 +718,5 @@
             return null;
         double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return "f/" + SimpleDecimalFormatterWithPrecision.format(fStop);
+        return getFStopDescription(fStop);
     }
 
@@ -721,5 +728,5 @@
             return null;
         double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return "f/" + SimpleDecimalFormatterWithPrecision.format(fStop);
+        return getFStopDescription(fStop);
     }
 
@@ -807,8 +814,5 @@
     {
         Rational value = _directory.getRational(TAG_FOCAL_LENGTH);
-        if (value == null)
-            return null;
-        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
-        return formatter.format(value.doubleValue()) + " mm";
+        return value == null ? null : getFocalLengthDescription(value.doubleValue());
     }
 
@@ -859,8 +863,5 @@
     public String getWhiteBalanceDescription()
     {
-        // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '4' flash,
-        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
-        // '21' D65, '22' D75, '255' other.
-        // see http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
+        // See http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
         final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
         if (value == null)
@@ -875,9 +876,9 @@
             case 10: return "Cloudy";
             case 11: return "Shade";
-            case 12: return "Daylight Flourescent";
-            case 13: return "Day White Flourescent";
-            case 14: return "Cool White Flourescent";
-            case 15: return "White Flourescent";
-            case 16: return "Warm White Flourescent";
+            case 12: return "Daylight Fluorescent";
+            case 13: return "Day White Fluorescent";
+            case 14: return "Cool White Fluorescent";
+            case 15: return "White Fluorescent";
+            case 16: return "Warm White Fluorescent";
             case 17: return "Standard light";
             case 18: return "Standard light (B)";
@@ -975,5 +976,5 @@
         if (value == null)
             return null;
-        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        DecimalFormat formatter = new DecimalFormat("0.0##");
         return formatter.format(value.doubleValue()) + " metres";
     }
@@ -1018,5 +1019,7 @@
             long apexPower10 = Math.round((double)apexPower * 10.0);
             float fApexPower = (float)apexPower10 / 10.0f;
-            return fApexPower + " sec";
+            DecimalFormat format = new DecimalFormat("0.##");
+            format.setRoundingMode(RoundingMode.HALF_UP);
+            return format.format(fApexPower) + " sec";
         } else {
             int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
@@ -1051,5 +1054,5 @@
         if (value == null)
             return null;
-        return "f/" + SimpleDecimalFormatterWithPrecision.format(value.doubleValue());
+        return getFStopDescription(value.doubleValue());
     }
 
Index: trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -140,4 +140,8 @@
     public static final int TAG_TILE_BYTE_COUNTS                  = 0x0145;
 
+    /**
+     * Tag is a pointer to one or more sub-IFDs.
+     + Seems to be used exclusively by raw formats, referencing one or two IFDs.
+     */
     public static final int TAG_SUB_IFD_OFFSET                    = 0x014a;
 
@@ -150,4 +154,6 @@
     public static final int TAG_YCBCR_POSITIONING                 = 0x0213;
     public static final int TAG_REFERENCE_BLACK_WHITE             = 0x0214;
+    public static final int TAG_STRIP_ROW_COUNTS                  = 0x022f;
+    public static final int TAG_APPLICATION_NOTES                 = 0x02bc;
 
     public static final int TAG_RELATED_IMAGE_FILE_FORMAT         = 0x1000;
@@ -260,5 +266,4 @@
     public static final int TAG_METERING_MODE                     = 0x9207;
 
-    public static final int TAG_LIGHT_SOURCE                      = 0x9208; // TODO duplicate tag
     /**
      * White balance (aka light source). '0' means unknown, '1' daylight,
@@ -267,5 +272,5 @@
      * '22' D75, '255' other.
      */
-    public static final int TAG_WHITE_BALANCE                     = 0x9208; // TODO duplicate tag
+    public static final int TAG_WHITE_BALANCE                     = 0x9208;
     /**
      * 0x0  = 0000000 = No Flash
@@ -600,6 +605,6 @@
         map.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
         map.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
-        map.put(TAG_MIN_SAMPLE_VALUE, "Minimum sample value");
-        map.put(TAG_MAX_SAMPLE_VALUE, "Maximum sample value");
+        map.put(TAG_MIN_SAMPLE_VALUE, "Minimum Sample Value");
+        map.put(TAG_MAX_SAMPLE_VALUE, "Maximum Sample Value");
         map.put(TAG_X_RESOLUTION, "X Resolution");
         map.put(TAG_Y_RESOLUTION, "Y Resolution");
@@ -627,4 +632,6 @@
         map.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
         map.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+        map.put(TAG_STRIP_ROW_COUNTS, "Strip Row Counts");
+        map.put(TAG_APPLICATION_NOTES, "Application Notes");
         map.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format");
         map.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width");
@@ -663,5 +670,4 @@
         map.put(TAG_SUBJECT_DISTANCE, "Subject Distance");
         map.put(TAG_METERING_MODE, "Metering Mode");
-        map.put(TAG_LIGHT_SOURCE, "Light Source");
         map.put(TAG_WHITE_BALANCE, "White Balance");
         map.put(TAG_FLASH, "Flash");
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,7 @@
 package com.drew.metadata.exif;
 
+import java.util.HashMap;
+
 import com.drew.lang.annotations.NotNull;
-
-import java.util.HashMap;
 
 /**
Index: trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifReader.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,8 +28,10 @@
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
@@ -60,5 +62,5 @@
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APP1);
+        return Collections.singletonList(JpegSegmentType.APP1);
     }
 
@@ -84,9 +86,15 @@
     public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset)
     {
+        extract(reader, metadata, readerOffset, null);
+    }
+
+    /** Reads TIFF formatted Exif data a specified offset within a {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset, @Nullable Directory parentDirectory)
+    {
         try {
             // Read the TIFF-formatted Exif data
             new TiffReader().processTiff(
                 reader,
-                new ExifTiffHandler(metadata, _storeThumbnailBytes),
+                new ExifTiffHandler(metadata, _storeThumbnailBytes, parentDirectory),
                 readerOffset
             );
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,7 +21,10 @@
 package com.drew.metadata.exif;
 
+import java.util.Date;
+import java.util.HashMap;
+import java.util.TimeZone;
+
 import com.drew.lang.annotations.NotNull;
-
-import java.util.HashMap;
+import com.drew.lang.annotations.Nullable;
 
 /**
@@ -61,3 +64,59 @@
         return _tagNameMap;
     }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was captured.  Attempts will be made to parse the
+     * values as though it is in the GMT {@link TimeZone}.
+     *
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateOriginal()
+    {
+        return getDateOriginal(null);
+    }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was captured.  Attempts will be made to parse the
+     * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter
+     * (if it is non-null).
+     *
+     * @param timeZone the time zone to use
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateOriginal(TimeZone timeZone)
+    {
+        return getDate(TAG_DATETIME_ORIGINAL, getString(TAG_SUBSECOND_TIME_ORIGINAL), timeZone);
+    }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was digitized.  Attempts will be made to parse the
+     * values as though it is in the GMT {@link TimeZone}.
+     *
+     * @return A Date object representing when this image was digitized, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateDigitized()
+    {
+        return getDateDigitized(null);
+    }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was digitized.  Attempts will be made to parse the
+     * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter
+     * (if it is non-null).
+     *
+     * @param timeZone the time zone to use
+     * @return A Date object representing when this image was digitized, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateDigitized(TimeZone timeZone)
+    {
+        return getDate(TAG_DATETIME_DIGITIZED, getString(TAG_SUBSECOND_TIME_DIGITIZED), timeZone);
+    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,8 +22,9 @@
 package com.drew.metadata.exif;
 
+import static com.drew.metadata.exif.ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH;
+import static com.drew.metadata.exif.ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET;
+
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
-
-import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
 
 /**
@@ -48,47 +49,6 @@
             case TAG_THUMBNAIL_LENGTH:
                 return getThumbnailLengthDescription();
-            case TAG_THUMBNAIL_COMPRESSION:
-                return getCompressionDescription();
             default:
                 return super.getDescription(tagType);
-        }
-    }
-
-    @Nullable
-    public String getCompressionDescription()
-    {
-        Integer value = _directory.getInteger(TAG_THUMBNAIL_COMPRESSION);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 1: return "Uncompressed";
-            case 2: return "CCITT 1D";
-            case 3: return "T4/Group 3 Fax";
-            case 4: return "T6/Group 4 Fax";
-            case 5: return "LZW";
-            case 6: return "JPEG (old-style)";
-            case 7: return "JPEG";
-            case 8: return "Adobe Deflate";
-            case 9: return "JBIG B&W";
-            case 10: return "JBIG Color";
-            case 32766: return "Next";
-            case 32771: return "CCIRLEW";
-            case 32773: return "PackBits";
-            case 32809: return "Thunderscan";
-            case 32895: return "IT8CTPAD";
-            case 32896: return "IT8LW";
-            case 32897: return "IT8MP";
-            case 32898: return "IT8BL";
-            case 32908: return "PixarFilm";
-            case 32909: return "PixarLog";
-            case 32946: return "Deflate";
-            case 32947: return "DCS";
-            case 32661: return "JBIG";
-            case 32676: return "SGILog";
-            case 32677: return "SGILog24";
-            case 32712: return "JPEG 2000";
-            case 32713: return "Nikon NEF Compressed";
-            default:
-                return "Unknown compression";
         }
     }
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,11 +22,11 @@
 package com.drew.metadata.exif;
 
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.MetadataException;
-
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.HashMap;
 
 /**
@@ -46,36 +46,4 @@
     public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
 
-    /**
-     * Shows compression method for Thumbnail.
-     * 1 = Uncompressed
-     * 2 = CCITT 1D
-     * 3 = T4/Group 3 Fax
-     * 4 = T6/Group 4 Fax
-     * 5 = LZW
-     * 6 = JPEG (old-style)
-     * 7 = JPEG
-     * 8 = Adobe Deflate
-     * 9 = JBIG B&amp;W
-     * 10 = JBIG Color
-     * 32766 = Next
-     * 32771 = CCIRLEW
-     * 32773 = PackBits
-     * 32809 = Thunderscan
-     * 32895 = IT8CTPAD
-     * 32896 = IT8LW
-     * 32897 = IT8MP
-     * 32898 = IT8BL
-     * 32908 = PixarFilm
-     * 32909 = PixarLog
-     * 32946 = Deflate
-     * 32947 = DCS
-     * 34661 = JBIG
-     * 34676 = SGILog
-     * 34677 = SGILog24
-     * 34712 = JPEG 2000
-     * 34713 = Nikon NEF Compressed
-     */
-    public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103;
-
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
@@ -85,5 +53,4 @@
         addExifTagNames(_tagNameMap);
 
-        _tagNameMap.put(TAG_THUMBNAIL_COMPRESSION, "Thumbnail Compression");
         _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
         _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
Index: trunk/src/com/drew/metadata/exif/ExifTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,4 +26,5 @@
 import com.drew.lang.SequentialByteArrayReader;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
@@ -47,8 +48,11 @@
     private final boolean _storeThumbnailBytes;
 
-    public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes)
+    public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes, @Nullable Directory parentDirectory)
     {
         super(metadata, ExifIFD0Directory.class);
         _storeThumbnailBytes = storeThumbnailBytes;
+
+        if (parentDirectory != null)
+            _currentDirectory.setParent(parentDirectory);
     }
 
@@ -65,15 +69,40 @@
     }
 
-    public boolean isTagIfdPointer(int tagType)
-    {
-        if (tagType == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
+    public boolean tryEnterSubIfd(int tagId)
+    {
+        if (tagId == ExifDirectoryBase.TAG_SUB_IFD_OFFSET) {
             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;
+        }
+
+        if (_currentDirectory instanceof ExifIFD0Directory) {
+            if (tagId == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET) {
+                pushDirectory(ExifSubIFDDirectory.class);
+                return true;
+            }
+
+            if (tagId == ExifIFD0Directory.TAG_GPS_INFO_OFFSET) {
+                pushDirectory(GpsDirectory.class);
+                return true;
+            }
+        }
+
+        if (_currentDirectory instanceof ExifSubIFDDirectory) {
+            if (tagId == ExifSubIFDDirectory.TAG_INTEROP_OFFSET) {
+                pushDirectory(ExifInteropDirectory.class);
+                return true;
+            }
+        }
+
+        if (_currentDirectory instanceof OlympusMakernoteDirectory) {
+            if (tagId == OlympusMakernoteDirectory.TAG_EQUIPMENT) {
+                pushDirectory(OlympusEquipmentMakernoteDirectory.class);
+                return true;
+            }
+
+            if (tagId == OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS) {
+                pushDirectory(OlympusCameraSettingsMakernoteDirectory.class);
+                return true;
+            }
         }
 
@@ -96,4 +125,13 @@
         // NOTE have seen the CanonMakernoteDirectory IFD have a follower pointer, but it points to invalid data.
         return false;
+    }
+
+    @Nullable
+    public Long tryCustomProcessFormat(final int tagId, final int formatCode, final long componentCount)
+    {
+        if (formatCode == 13)
+            return componentCount * 4;
+
+        return null;
     }
 
@@ -115,5 +153,5 @@
             if (reader.getInt8(tagOffset) == 0x1c) {
                 final byte[] iptcBytes = reader.getBytes(tagOffset, byteCount);
-                new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length);
+                new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length, _currentDirectory);
                 return true;
             }
@@ -129,5 +167,5 @@
             // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
             ExifThumbnailDirectory thumbnailDirectory = _metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
-            if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
+            if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_COMPRESSION)) {
                 Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
                 Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
@@ -164,13 +202,20 @@
         final String firstSevenChars = reader.getString(makernoteOffset, 7);
         final String firstEightChars = reader.getString(makernoteOffset, 8);
+        final String firstTenChars = reader.getString(makernoteOffset, 10);
         final String firstTwelveChars = reader.getString(makernoteOffset, 12);
 
         boolean byteOrderBefore = reader.isMotorolaByteOrder();
 
-        if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
+        if ("OLYMP\0".equals(firstSixChars) || "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 ("OLYMPUS\0II".equals(firstTenChars)) {
+            // Olympus Makernote (alternate)
+            // Note that data is relative to the beginning of the makernote
+            // http://exiv2.org/makernote.html
+            pushDirectory(OlympusMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, makernoteOffset);
         } 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
Index: trunk/src/com/drew/metadata/exif/GpsDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -110,5 +110,5 @@
         // time in hour, min, sec
         Rational[] timeComponents = _directory.getRationalArray(TAG_TIME_STAMP);
-        DecimalFormat df = new DecimalFormat("00.00");
+        DecimalFormat df = new DecimalFormat("00.000");
         return timeComponents == null
             ? null
Index: trunk/src/com/drew/metadata/exif/GpsDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,5 +26,10 @@
 import com.drew.lang.annotations.Nullable;
 
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.Locale;
 
 /**
@@ -164,8 +169,8 @@
     public GeoLocation getGeoLocation()
     {
-        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);
+        Rational[] latitudes = getRationalArray(TAG_LATITUDE);
+        Rational[] longitudes = getRationalArray(TAG_LONGITUDE);
+        String latitudeRef = getString(TAG_LATITUDE_REF);
+        String longitudeRef = getString(TAG_LONGITUDE_REF);
 
         // Make sure we have the required values
@@ -186,3 +191,31 @@
         return new GeoLocation(lat, lon);
     }
+
+    /**
+     * Parses the date stamp tag and the time stamp tag to obtain a single Date object representing the
+     * date and time when this image was captured.
+     *
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getGpsDate()
+    {
+        String date = getString(TAG_DATE_STAMP);
+        Rational[] timeComponents = getRationalArray(TAG_TIME_STAMP);
+
+        // Make sure we have the required values
+        if (date == null)
+            return null;
+        if (timeComponents == null || timeComponents.length != 3)
+            return null;
+
+        String dateTime = String.format(Locale.US, "%s %02d:%02d:%02.3f UTC",
+            date, timeComponents[0].intValue(), timeComponents[1].intValue(), timeComponents[2].doubleValue());
+        try {
+            DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss.S z");
+            return parser.parse(dateTime);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
 }
Index: trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,4 +24,6 @@
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
+
+import java.text.DecimalFormat;
 
 import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
@@ -138,4 +140,5 @@
     public String getSerialNumberDescription()
     {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html
         Integer value = _directory.getInteger(TAG_CANON_SERIAL_NUMBER);
         if (value == null)
@@ -673,6 +676,6 @@
             return "Self timer not used";
         } else {
-            // TODO find an image that tests this calculation
-            return Double.toString((double)value * 0.1d) + " sec";
+            DecimalFormat format = new DecimalFormat("0.##");
+            return format.format((double)value * 0.1d) + " sec";
         }
     }
Index: trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -155,9 +155,5 @@
     {
         Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE);
-
-        if (value == null)
-            return null;
-
-        return value + " mm";
+        return value == null ? null : getFocalLengthDescription(value);
     }
 
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -240,7 +240,5 @@
     {
         Double value = _directory.getDoubleObject(TAG_FOCAL_LENGTH);
-        if (value == null)
-            return null;
-        return Double.toString(value / 10d) + " mm";
+        return value == null ? null : getFocalLengthDescription(value / 10d);
     }
 
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -306,5 +306,5 @@
     {
         int[] values = _directory.getIntArray(tagType);
-        if (values == null)
+        if (values == null || values.length < 2)
             return null;
         if (values.length < 3 || values[2] == 0)
@@ -329,12 +329,5 @@
     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());
-
+        return getLensSpecificationDescription(TAG_LENS);
     }
 
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java	(revision 10862)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java	(revision 10862)
@@ -0,0 +1,1357 @@
+/*
+ * 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 java.util.HashMap;
+
+import static com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusCameraSettingsMakernoteDirectory}.
+ * <p>
+ * Some Description functions and the Extender and Lens types lists converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusCameraSettingsMakernoteDescriptor extends TagDescriptor<OlympusCameraSettingsMakernoteDirectory>
+{
+    public OlympusCameraSettingsMakernoteDescriptor(@NotNull OlympusCameraSettingsMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagCameraSettingsVersion:
+                return getCameraSettingsVersionDescription();
+            case TagPreviewImageValid:
+                return getPreviewImageValidDescription();
+
+            case TagExposureMode:
+                return getExposureModeDescription();
+            case TagAeLock:
+                return getAeLockDescription();
+            case TagMeteringMode:
+                return getMeteringModeDescription();
+            case TagExposureShift:
+                return getExposureShiftDescription();
+            case TagNdFilter:
+                return getNdFilterDescription();
+
+            case TagMacroMode:
+                return getMacroModeDescription();
+            case TagFocusMode:
+                return getFocusModeDescription();
+            case TagFocusProcess:
+                return getFocusProcessDescription();
+            case TagAfSearch:
+                return getAfSearchDescription();
+            case TagAfAreas:
+                return getAfAreasDescription();
+            case TagAfPointSelected:
+                return getAfPointSelectedDescription();
+            case TagAfFineTune:
+                return getAfFineTuneDescription();
+
+            case TagFlashMode:
+                return getFlashModeDescription();
+            case TagFlashRemoteControl:
+                return getFlashRemoteControlDescription();
+            case TagFlashControlMode:
+                return getFlashControlModeDescription();
+            case TagFlashIntensity:
+                return getFlashIntensityDescription();
+            case TagManualFlashStrength:
+                return getManualFlashStrengthDescription();
+
+            case TagWhiteBalance2:
+                return getWhiteBalance2Description();
+            case TagWhiteBalanceTemperature:
+                return getWhiteBalanceTemperatureDescription();
+            case TagCustomSaturation:
+                return getCustomSaturationDescription();
+            case TagModifiedSaturation:
+                return getModifiedSaturationDescription();
+            case TagContrastSetting:
+                return getContrastSettingDescription();
+            case TagSharpnessSetting:
+                return getSharpnessSettingDescription();
+            case TagColorSpace:
+                return getColorSpaceDescription();
+            case TagSceneMode:
+                return getSceneModeDescription();
+            case TagNoiseReduction:
+                return getNoiseReductionDescription();
+            case TagDistortionCorrection:
+                return getDistortionCorrectionDescription();
+            case TagShadingCompensation:
+                return getShadingCompensationDescription();
+            case TagGradation:
+                return getGradationDescription();
+            case TagPictureMode:
+                return getPictureModeDescription();
+            case TagPictureModeSaturation:
+                return getPictureModeSaturationDescription();
+            case TagPictureModeContrast:
+                return getPictureModeContrastDescription();
+            case TagPictureModeSharpness:
+                return getPictureModeSharpnessDescription();
+            case TagPictureModeBWFilter:
+                return getPictureModeBWFilterDescription();
+            case TagPictureModeTone:
+                return getPictureModeToneDescription();
+            case TagNoiseFilter:
+                return getNoiseFilterDescription();
+            case TagArtFilter:
+                return getArtFilterDescription();
+            case TagMagicFilter:
+                return getMagicFilterDescription();
+            case TagPictureModeEffect:
+                return getPictureModeEffectDescription();
+            case TagToneLevel:
+                return getToneLevelDescription();
+            case TagArtFilterEffect:
+                return getArtFilterEffectDescription();
+
+            case TagDriveMode:
+                return getDriveModeDescription();
+            case TagPanoramaMode:
+                return getPanoramaModeDescription();
+            case TagImageQuality2:
+                return getImageQuality2Description();
+            case TagImageStabilization:
+                return getImageStabilizationDescription();
+
+            case TagStackedImage:
+                return getStackedImageDescription();
+
+            case TagManometerPressure:
+                return getManometerPressureDescription();
+            case TagManometerReading:
+                return getManometerReadingDescription();
+            case TagExtendedWBDetect:
+                return getExtendedWBDetectDescription();
+            case TagRollAngle:
+                return getRollAngleDescription();
+            case TagPitchAngle:
+                return getPitchAngleDescription();
+            case TagDateTimeUtc:
+                return getDateTimeUTCDescription();
+
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getCameraSettingsVersionDescription()
+    {
+        return getVersionBytesDescription(TagCameraSettingsVersion, 4);
+    }
+
+    @Nullable
+    public String getPreviewImageValidDescription()
+    {
+        return getIndexedDescription(TagPreviewImageValid,
+            "No", "Yes");
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(TagExposureMode, 1,
+            "Manual", "Program", "Aperture-priority AE", "Shutter speed priority", "Program-shift");
+    }
+
+    @Nullable
+    public String getAeLockDescription()
+    {
+        return getIndexedDescription(TagAeLock,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        Integer value = _directory.getInteger(TagMeteringMode);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 2:
+                return "Center-weighted average";
+            case 3:
+                return "Spot";
+            case 5:
+                return "ESP";
+            case 261:
+                return "Pattern+AF";
+            case 515:
+                return "Spot+Highlight control";
+            case 1027:
+                return "Spot+Shadow control";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getExposureShiftDescription()
+    {
+        return getRationalOrDoubleString(TagExposureShift);
+    }
+
+    @Nullable
+    public String getNdFilterDescription()
+    {
+        return getIndexedDescription(TagNdFilter, "Off", "On");
+    }
+
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(TagMacroMode, "Off", "On", "Super Macro");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagFocusMode);
+        if (values == null) {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagFocusMode);
+            if (value == null)
+                return null;
+
+            values = new int[]{value};
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        switch (values[0]) {
+            case 0:
+                sb.append("Single AF");
+                break;
+            case 1:
+                sb.append("Sequential shooting AF");
+                break;
+            case 2:
+                sb.append("Continuous AF");
+                break;
+            case 3:
+                sb.append("Multi AF");
+                break;
+            case 4:
+                sb.append("Face detect");
+                break;
+            case 10:
+                sb.append("MF");
+                break;
+            default:
+                sb.append("Unknown (" + values[0] + ")");
+                break;
+        }
+
+        if (values.length > 1) {
+            sb.append("; ");
+            int value1 = values[1];
+
+            if (value1 == 0) {
+                sb.append("(none)");
+            } else {
+                if (( value1       & 1) > 0) sb.append("S-AF, ");
+                if (((value1 >> 2) & 1) > 0) sb.append("C-AF, ");
+                if (((value1 >> 4) & 1) > 0) sb.append("MF, ");
+                if (((value1 >> 5) & 1) > 0) sb.append("Face detect, ");
+                if (((value1 >> 6) & 1) > 0) sb.append("Imager AF, ");
+                if (((value1 >> 7) & 1) > 0) sb.append("Live View Magnification Frame, ");
+                if (((value1 >> 8) & 1) > 0) sb.append("AF sensor, ");
+
+                sb.setLength(sb.length() - 2);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getFocusProcessDescription()
+    {
+        int[] values = _directory.getIntArray(TagFocusProcess);
+        if (values == null) {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagFocusProcess);
+            if (value == null)
+                return null;
+
+            values = new int[]{value};
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        switch (values[0]) {
+            case 0:
+                sb.append("AF not used");
+                break;
+            case 1:
+                sb.append("AF used");
+                break;
+            default:
+                sb.append("Unknown (" + values[0] + ")");
+                break;
+        }
+
+        if (values.length > 1)
+            sb.append("; " + values[1]);
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getAfSearchDescription()
+    {
+        return getIndexedDescription(TagAfSearch, "Not Ready", "Ready");
+    }
+
+    /// <summary>
+    /// coordinates range from 0 to 255
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getAfAreasDescription()
+    {
+        Object obj = _directory.getObject(TagAfAreas);
+        if (obj == null || !(obj instanceof long[]))
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (long point : (long[]) obj) {
+            if (point == 0L)
+                continue;
+            if (sb.length() != 0)
+                sb.append(", ");
+
+            if (point == 0x36794285L)
+                sb.append("Left ");
+            else if (point == 0x79798585L)
+                sb.append("Center ");
+            else if (point == 0xBD79C985L)
+                sb.append("Right ");
+
+            sb.append(String.format("(%d/255,%d/255)-(%d/255,%d/255)",
+                (point >> 24) & 0xFF,
+                (point >> 16) & 0xFF,
+                (point >> 8) & 0xFF,
+                point & 0xFF));
+        }
+
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    /// <summary>
+    /// coordinates expressed as a percent
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getAfPointSelectedDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TagAfPointSelected);
+        if (values == null)
+            return "n/a";
+
+        if (values.length < 4)
+            return null;
+
+        int index = 0;
+        if (values.length == 5 && values[0].longValue() == 0)
+            index = 1;
+
+        int p1 = (int)(values[index].doubleValue() * 100);
+        int p2 = (int)(values[index + 1].doubleValue() * 100);
+        int p3 = (int)(values[index + 2].doubleValue() * 100);
+        int p4 = (int)(values[index + 3].doubleValue() * 100);
+
+        return String.format("(%d%%,%d%%) (%d%%,%d%%)", p1, p2, p3, p4);
+
+    }
+
+    @Nullable
+    public String getAfFineTuneDescription()
+    {
+        return getIndexedDescription(TagAfFineTune, "Off", "On");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(TagFlashMode);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "Off";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if (( v       & 1) != 0) sb.append("On, ");
+        if (((v >> 1) & 1) != 0) sb.append("Fill-in, ");
+        if (((v >> 2) & 1) != 0) sb.append("Red-eye, ");
+        if (((v >> 3) & 1) != 0) sb.append("Slow-sync, ");
+        if (((v >> 4) & 1) != 0) sb.append("Forced On, ");
+        if (((v >> 5) & 1) != 0) sb.append("2nd Curtain, ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getFlashRemoteControlDescription()
+    {
+        Integer value = _directory.getInteger(TagFlashRemoteControl);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0:
+                return "Off";
+            case 0x01:
+                return "Channel 1, Low";
+            case 0x02:
+                return "Channel 2, Low";
+            case 0x03:
+                return "Channel 3, Low";
+            case 0x04:
+                return "Channel 4, Low";
+            case 0x09:
+                return "Channel 1, Mid";
+            case 0x0a:
+                return "Channel 2, Mid";
+            case 0x0b:
+                return "Channel 3, Mid";
+            case 0x0c:
+                return "Channel 4, Mid";
+            case 0x11:
+                return "Channel 1, High";
+            case 0x12:
+                return "Channel 2, High";
+            case 0x13:
+                return "Channel 3, High";
+            case 0x14:
+                return "Channel 4, High";
+
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    /// <summary>
+    /// 3 or 4 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getFlashControlModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagFlashControlMode);
+        if (values == null)
+            return null;
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        switch (values[0]) {
+            case 0:
+                sb.append("Off");
+                break;
+            case 3:
+                sb.append("TTL");
+                break;
+            case 4:
+                sb.append("Auto");
+                break;
+            case 5:
+                sb.append("Manual");
+                break;
+            default:
+                sb.append("Unknown (").append(values[0]).append(")");
+                break;
+        }
+
+        for (int i = 1; i < values.length; i++)
+            sb.append("; ").append(values[i]);
+
+        return sb.toString();
+    }
+
+    /// <summary>
+    /// 3 or 4 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getFlashIntensityDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TagFlashIntensity);
+        if (values == null || values.length == 0)
+            return null;
+
+        if (values.length == 3) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0)
+                return "n/a";
+        } else if (values.length == 4) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0)
+                return "n/a (x4)";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Rational t : values)
+            sb.append(t).append(", ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getManualFlashStrengthDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TagManualFlashStrength);
+        if (values == null || values.length == 0)
+            return "n/a";
+
+        if (values.length == 3) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0)
+                return "n/a";
+        } else if (values.length == 4) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0)
+                return "n/a (x4)";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Rational t : values)
+            sb.append(t).append(", ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getWhiteBalance2Description()
+    {
+        Integer value = _directory.getInteger(TagWhiteBalance2);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0:
+                return "Auto";
+            case 1:
+                return "Auto (Keep Warm Color Off)";
+            case 16:
+                return "7500K (Fine Weather with Shade)";
+            case 17:
+                return "6000K (Cloudy)";
+            case 18:
+                return "5300K (Fine Weather)";
+            case 20:
+                return "3000K (Tungsten light)";
+            case 21:
+                return "3600K (Tungsten light-like)";
+            case 22:
+                return "Auto Setup";
+            case 23:
+                return "5500K (Flash)";
+            case 33:
+                return "6600K (Daylight fluorescent)";
+            case 34:
+                return "4500K (Neutral white fluorescent)";
+            case 35:
+                return "4000K (Cool white fluorescent)";
+            case 36:
+                return "White Fluorescent";
+            case 48:
+                return "3600K (Tungsten light-like)";
+            case 67:
+                return "Underwater";
+            case 256:
+                return "One Touch WB 1";
+            case 257:
+                return "One Touch WB 2";
+            case 258:
+                return "One Touch WB 3";
+            case 259:
+                return "One Touch WB 4";
+            case 512:
+                return "Custom WB 1";
+            case 513:
+                return "Custom WB 2";
+            case 514:
+                return "Custom WB 3";
+            case 515:
+                return "Custom WB 4";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceTemperatureDescription()
+    {
+        Integer value = _directory.getInteger(TagWhiteBalanceTemperature);
+        if (value == null)
+            return null;
+        if (value == 0)
+            return "Auto";
+        return value.toString();
+    }
+
+    @Nullable
+    public String getCustomSaturationDescription()
+    {
+        // TODO: if model is /^E-1\b/  then
+        // $a-=$b; $c-=$b;
+        // return "CS$a (min CS0, max CS$c)"
+        return getValueMinMaxDescription(TagCustomSaturation);
+    }
+
+    @Nullable
+    public String getModifiedSaturationDescription()
+    {
+        return getIndexedDescription(TagModifiedSaturation,
+            "Off", "CM1 (Red Enhance)", "CM2 (Green Enhance)", "CM3 (Blue Enhance)", "CM4 (Skin Tones)");
+    }
+
+    @Nullable
+    public String getContrastSettingDescription()
+    {
+        return getValueMinMaxDescription(TagContrastSetting);
+    }
+
+    @Nullable
+    public String getSharpnessSettingDescription()
+    {
+        return getValueMinMaxDescription(TagSharpnessSetting);
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        return getIndexedDescription(TagColorSpace,
+            "sRGB", "Adobe RGB", "Pro Photo RGB");
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(TagSceneMode);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0:
+                return "Standard";
+            case 6:
+                return "Auto";
+            case 7:
+                return "Sport";
+            case 8:
+                return "Portrait";
+            case 9:
+                return "Landscape+Portrait";
+            case 10:
+                return "Landscape";
+            case 11:
+                return "Night Scene";
+            case 12:
+                return "Self Portrait";
+            case 13:
+                return "Panorama";
+            case 14:
+                return "2 in 1";
+            case 15:
+                return "Movie";
+            case 16:
+                return "Landscape+Portrait";
+            case 17:
+                return "Night+Portrait";
+            case 18:
+                return "Indoor";
+            case 19:
+                return "Fireworks";
+            case 20:
+                return "Sunset";
+            case 21:
+                return "Beauty Skin";
+            case 22:
+                return "Macro";
+            case 23:
+                return "Super Macro";
+            case 24:
+                return "Food";
+            case 25:
+                return "Documents";
+            case 26:
+                return "Museum";
+            case 27:
+                return "Shoot & Select";
+            case 28:
+                return "Beach & Snow";
+            case 29:
+                return "Self Portrait+Timer";
+            case 30:
+                return "Candle";
+            case 31:
+                return "Available Light";
+            case 32:
+                return "Behind Glass";
+            case 33:
+                return "My Mode";
+            case 34:
+                return "Pet";
+            case 35:
+                return "Underwater Wide1";
+            case 36:
+                return "Underwater Macro";
+            case 37:
+                return "Shoot & Select1";
+            case 38:
+                return "Shoot & Select2";
+            case 39:
+                return "High Key";
+            case 40:
+                return "Digital Image Stabilization";
+            case 41:
+                return "Auction";
+            case 42:
+                return "Beach";
+            case 43:
+                return "Snow";
+            case 44:
+                return "Underwater Wide2";
+            case 45:
+                return "Low Key";
+            case 46:
+                return "Children";
+            case 47:
+                return "Vivid";
+            case 48:
+                return "Nature Macro";
+            case 49:
+                return "Underwater Snapshot";
+            case 50:
+                return "Shooting Guide";
+            case 54:
+                return "Face Portrait";
+            case 57:
+                return "Bulb";
+            case 59:
+                return "Smile Shot";
+            case 60:
+                return "Quick Shutter";
+            case 63:
+                return "Slow Shutter";
+            case 64:
+                return "Bird Watching";
+            case 65:
+                return "Multiple Exposure";
+            case 66:
+                return "e-Portrait";
+            case 67:
+                return "Soft Background Shot";
+            case 142:
+                return "Hand-held Starlight";
+            case 154:
+                return "HDR";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TagNoiseReduction);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if ((v & 1) != 0) sb.append("Noise Reduction, ");
+        if (((v >> 1) & 1) != 0) sb.append("Noise Filter, ");
+        if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), ");
+        if (((v >> 3) & 1) != 0) sb.append("Auto, ");
+
+        return sb.length() != 0
+            ? sb.substring(0, sb.length() - 2)
+            : "(none)";
+    }
+
+    @Nullable
+    public String getDistortionCorrectionDescription()
+    {
+        return getIndexedDescription(TagDistortionCorrection, "Off", "On");
+    }
+
+    @Nullable
+    public String getShadingCompensationDescription()
+    {
+        return getIndexedDescription(TagShadingCompensation, "Off", "On");
+    }
+
+    /// <summary>
+    /// 3 or 4 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getGradationDescription()
+    {
+        int[] values = _directory.getIntArray(TagGradation);
+        if (values == null || values.length < 3)
+            return null;
+
+        String join = String.format("%d %d %d", values[0], values[1], values[2]);
+
+        String ret;
+        if (join.equals("0 0 0")) {
+            ret = "n/a";
+        } else if (join.equals("-1 -1 1")) {
+            ret = "Low Key";
+        } else if (join.equals("0 -1 1")) {
+            ret = "Normal";
+        } else if (join.equals("1 -1 1")) {
+            ret = "High Key";
+        } else {
+            ret = "Unknown (" + join + ")";
+        }
+
+        if (values.length > 3) {
+            if (values[3] == 0)
+                ret += "; User-Selected";
+            else if (values[3] == 1)
+                ret += "; Auto-Override";
+        }
+
+        return ret;
+    }
+
+    /// <summary>
+    /// 1 or 2 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getPictureModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagPictureMode);
+        if (values == null) {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagNoiseReduction);
+            if (value == null)
+                return null;
+
+            values = new int[]{value};
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        switch (values[0]) {
+            case 1:
+                sb.append("Vivid");
+                break;
+            case 2:
+                sb.append("Natural");
+                break;
+            case 3:
+                sb.append("Muted");
+                break;
+            case 4:
+                sb.append("Portrait");
+                break;
+            case 5:
+                sb.append("i-Enhance");
+                break;
+            case 256:
+                sb.append("Monotone");
+                break;
+            case 512:
+                sb.append("Sepia");
+                break;
+            default:
+                sb.append("Unknown (").append(values[0]).append(")");
+                break;
+        }
+
+        if (values.length > 1)
+            sb.append("; ").append(values[1]);
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getPictureModeSaturationDescription()
+    {
+        return getValueMinMaxDescription(TagPictureModeSaturation);
+    }
+
+    @Nullable
+    public String getPictureModeContrastDescription()
+    {
+        return getValueMinMaxDescription(TagPictureModeContrast);
+    }
+
+    @Nullable
+    public String getPictureModeSharpnessDescription()
+    {
+        return getValueMinMaxDescription(TagPictureModeSharpness);
+    }
+
+    @Nullable
+    public String getPictureModeBWFilterDescription()
+    {
+        return getIndexedDescription(TagPictureModeBWFilter,
+            "n/a", "Neutral", "Yellow", "Orange", "Red", "Green");
+    }
+
+    @Nullable
+    public String getPictureModeToneDescription()
+    {
+        return getIndexedDescription(TagPictureModeTone,
+            "n/a", "Neutral", "Sepia", "Blue", "Purple", "Green");
+    }
+
+    @Nullable
+    public String getNoiseFilterDescription()
+    {
+        int[] values = _directory.getIntArray(TagNoiseFilter);
+        if (values == null)
+            return null;
+
+        String join = String.format("%d %d %d", values[0], values[1], values[2]);
+
+        if (join.equals("0 0 0"))
+            return "n/a";
+        if (join.equals("-2 -2 1"))
+            return "Off";
+        if (join.equals("-1 -2 1"))
+            return "Low";
+        if (join.equals("0 -2 1"))
+            return "Standard";
+        if (join.equals("1 -2 1"))
+            return "High";
+        return "Unknown (" + join + ")";
+    }
+
+    @Nullable
+    public String getArtFilterDescription()
+    {
+        return getFiltersDescription(TagArtFilter);
+    }
+
+    @Nullable
+    public String getMagicFilterDescription()
+    {
+        return getFiltersDescription(TagMagicFilter);
+    }
+
+    @Nullable
+    public String getPictureModeEffectDescription()
+    {
+        int[] values = _directory.getIntArray(TagPictureModeEffect);
+        if (values == null)
+            return null;
+
+        String key = String.format("%d %d %d", values[0], values[1], values[2]);
+        if (key.equals("0 0 0"))
+            return "n/a";
+        if (key.equals("-1 -1 1"))
+            return "Low";
+        if (key.equals("0 -1 1"))
+            return "Standard";
+        if (key.equals("1 -1 1"))
+            return "High";
+        return "Unknown (" + key + ")";
+    }
+
+    @Nullable
+    public String getToneLevelDescription()
+    {
+        int[] values = _directory.getIntArray(TagToneLevel);
+        if (values == null || values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 1)
+                sb.append("Highlights ");
+            else if (i == 5)
+                sb.append("Shadows ");
+
+            sb.append(values[i]).append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getArtFilterEffectDescription()
+    {
+        int[] values = _directory.getIntArray(TagArtFilterEffect);
+        if (values == null || values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0) {
+                sb.append((_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]"));
+            } else if (i == 4) {
+                switch (values[i]) {
+                    case 0x0000:
+                        sb.append("No Effect");
+                        break;
+                    case 0x8010:
+                        sb.append("Star Light");
+                        break;
+                    case 0x8020:
+                        sb.append("Pin Hole");
+                        break;
+                    case 0x8030:
+                        sb.append("Frame");
+                        break;
+                    case 0x8040:
+                        sb.append("Soft Focus");
+                        break;
+                    case 0x8050:
+                        sb.append("White Edge");
+                        break;
+                    case 0x8060:
+                        sb.append("B&W");
+                        break;
+                    default:
+                        sb.append("Unknown (").append(values[i]).append(")");
+                        break;
+                }
+            } else {
+                sb.append(values[i]);
+            }
+            sb.append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    /// <summary>
+    /// 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getDriveModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagDriveMode);
+        if (values == null)
+            return null;
+
+        if (values.length == 0 || values[0] == 0)
+            return "Single Shot";
+
+        StringBuilder a = new StringBuilder();
+
+        if (values[0] == 5 && values.length >= 3) {
+            int c = values[2];
+            if (( c       & 1) > 0) a.append("AE");
+            if (((c >> 1) & 1) > 0) a.append("WB");
+            if (((c >> 2) & 1) > 0) a.append("FL");
+            if (((c >> 3) & 1) > 0) a.append("MF");
+            if (((c >> 6) & 1) > 0) a.append("Focus");
+
+            a.append(" Bracketing");
+        } else {
+            switch (values[0]) {
+                case 1:
+                    a.append("Continuous Shooting");
+                    break;
+                case 2:
+                    a.append("Exposure Bracketing");
+                    break;
+                case 3:
+                    a.append("White Balance Bracketing");
+                    break;
+                case 4:
+                    a.append("Exposure+WB Bracketing");
+                    break;
+                default:
+                    a.append("Unknown (").append(values[0]).append(")");
+                    break;
+            }
+        }
+
+        a.append(", Shot ").append(values[1]);
+
+        return a.toString();
+    }
+
+    /// <summary>
+    /// 2 numbers: 1. Mode, 2. Shot number
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getPanoramaModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagPanoramaMode);
+        if (values == null)
+            return null;
+
+        if (values.length == 0 || values[0] == 0)
+            return "Off";
+
+        String a;
+        switch (values[0]) {
+            case 1:
+                a = "Left to Right";
+                break;
+            case 2:
+                a = "Right to Left";
+                break;
+            case 3:
+                a = "Bottom to Top";
+                break;
+            case 4:
+                a = "Top to Bottom";
+                break;
+            default:
+                a = "Unknown (" + values[0] + ")";
+                break;
+        }
+
+        return String.format("%s, Shot %d", a, values[1]);
+    }
+
+    @Nullable
+    public String getImageQuality2Description()
+    {
+        return getIndexedDescription(TagImageQuality2, 1,
+            "SQ", "HQ", "SHQ", "RAW", "SQ (5)");
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        return getIndexedDescription(TagImageStabilization,
+            "Off", "On, Mode 1", "On, Mode 2", "On, Mode 3", "On, Mode 4");
+    }
+
+    @Nullable
+    public String getStackedImageDescription()
+    {
+        int[] values = _directory.getIntArray(TagStackedImage);
+        if (values == null || values.length < 2)
+            return null;
+
+        int v1 = values[0];
+        int v2 = values[1];
+
+        if (v1 == 0 && v2 == 0)
+            return "No";
+        if (v1 == 9 && v2 == 8)
+            return "Focus-stacked (8 images)";
+
+        return String.format("Unknown (%d %d)", v1, v2);
+    }
+
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getManometerPressureDescription()
+    {
+        Integer value = _directory.getInteger(TagManometerPressure);
+        if (value == null)
+            return null;
+
+        return String.format("%s kPa", new DecimalFormat("#.##").format(value / 10.0));
+    }
+
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getManometerReadingDescription()
+    {
+        int[] values = _directory.getIntArray(TagManometerReading);
+        if (values == null || values.length < 2)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("#.##");
+        return String.format("%s m, %s ft",
+            format.format(values[0] / 10.0),
+            format.format(values[1] / 10.0));
+    }
+
+    @Nullable
+    public String getExtendedWBDetectDescription()
+    {
+        return getIndexedDescription(TagExtendedWBDetect, "Off", "On");
+    }
+
+    /// <summary>
+    /// converted to degrees of clockwise camera rotation
+    /// </summary>
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getRollAngleDescription()
+    {
+        int[] values = _directory.getIntArray(TagRollAngle);
+        if (values == null || values.length < 2)
+            return null;
+
+        String ret = values[0] != 0
+            ? Double.toString(-values[0] / 10.0)
+            : "n/a";
+
+        return String.format("%s %d", ret, values[1]);
+    }
+
+    /// <summary>
+    /// converted to degrees of upward camera tilt
+    /// </summary>
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getPitchAngleDescription()
+    {
+        int[] values = _directory.getIntArray(TagPitchAngle);
+        if (values == null || values.length < 2)
+            return null;
+
+        // (second value is 0 if level gauge is off)
+        String ret = values[0] != 0
+            ? Double.toString(values[0] / 10.0)
+            : "n/a";
+
+        return String.format("%s %d", ret, values[1]);
+    }
+
+    @Nullable
+    public String getDateTimeUTCDescription()
+    {
+        Object value = _directory.getObject(TagDateTimeUtc);
+        if (value == null)
+            return null;
+        return value.toString();
+    }
+
+    @Nullable
+    private String getValueMinMaxDescription(int tagId)
+    {
+        int[] values = _directory.getIntArray(tagId);
+        if (values == null || values.length < 3)
+            return null;
+
+        return String.format("%d (min %d, max %d)", values[0], values[1], values[2]);
+    }
+
+    private String getFiltersDescription(int tagId)
+    {
+        int[] values = _directory.getIntArray(tagId);
+        if (values == null || values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0)
+                sb.append(_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]");
+            else
+                sb.append(values[i]);
+            sb.append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    // ArtFilter, ArtFilterEffect and MagicFilter values
+    private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>();
+
+    static {
+        _filters.put(0, "Off");
+        _filters.put(1, "Soft Focus");
+        _filters.put(2, "Pop Art");
+        _filters.put(3, "Pale & Light Color");
+        _filters.put(4, "Light Tone");
+        _filters.put(5, "Pin Hole");
+        _filters.put(6, "Grainy Film");
+        _filters.put(9, "Diorama");
+        _filters.put(10, "Cross Process");
+        _filters.put(12, "Fish Eye");
+        _filters.put(13, "Drawing");
+        _filters.put(14, "Gentle Sepia");
+        _filters.put(15, "Pale & Light Color II");
+        _filters.put(16, "Pop Art II");
+        _filters.put(17, "Pin Hole II");
+        _filters.put(18, "Pin Hole III");
+        _filters.put(19, "Grainy Film II");
+        _filters.put(20, "Dramatic Tone");
+        _filters.put(21, "Punk");
+        _filters.put(22, "Soft Focus 2");
+        _filters.put(23, "Sparkle");
+        _filters.put(24, "Watercolor");
+        _filters.put(25, "Key Line");
+        _filters.put(26, "Key Line II");
+        _filters.put(27, "Miniature");
+        _filters.put(28, "Reflection");
+        _filters.put(29, "Fragmented");
+        _filters.put(31, "Cross Process II");
+        _filters.put(32, "Dramatic Tone II");
+        _filters.put(33, "Watercolor I");
+        _filters.put(34, "Watercolor II");
+        _filters.put(35, "Diorama II");
+        _filters.put(36, "Vintage");
+        _filters.put(37, "Vintage II");
+        _filters.put(38, "Vintage III");
+        _filters.put(39, "Partial Color");
+        _filters.put(40, "Partial Color II");
+        _filters.put(41, "Partial Color III");
+    }
+}
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java	(revision 10862)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java	(revision 10862)
@@ -0,0 +1,198 @@
+/*
+ * 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;
+
+/**
+ * The Olympus camera settings makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusCameraSettingsMakernoteDirectory extends Directory
+{
+    public static final int TagCameraSettingsVersion = 0x0000;
+    public static final int TagPreviewImageValid = 0x0100;
+    public static final int TagPreviewImageStart = 0x0101;
+    public static final int TagPreviewImageLength = 0x0102;
+
+    public static final int TagExposureMode = 0x0200;
+    public static final int TagAeLock = 0x0201;
+    public static final int TagMeteringMode = 0x0202;
+    public static final int TagExposureShift = 0x0203;
+    public static final int TagNdFilter = 0x0204;
+
+    public static final int TagMacroMode = 0x0300;
+    public static final int TagFocusMode = 0x0301;
+    public static final int TagFocusProcess = 0x0302;
+    public static final int TagAfSearch = 0x0303;
+    public static final int TagAfAreas = 0x0304;
+    public static final int TagAfPointSelected = 0x0305;
+    public static final int TagAfFineTune = 0x0306;
+    public static final int TagAfFineTuneAdj = 0x0307;
+
+    public static final int TagFlashMode = 0x400;
+    public static final int TagFlashExposureComp = 0x401;
+    public static final int TagFlashRemoteControl = 0x403;
+    public static final int TagFlashControlMode = 0x404;
+    public static final int TagFlashIntensity = 0x405;
+    public static final int TagManualFlashStrength = 0x406;
+
+    public static final int TagWhiteBalance2 = 0x500;
+    public static final int TagWhiteBalanceTemperature = 0x501;
+    public static final int TagWhiteBalanceBracket = 0x502;
+    public static final int TagCustomSaturation = 0x503;
+    public static final int TagModifiedSaturation = 0x504;
+    public static final int TagContrastSetting = 0x505;
+    public static final int TagSharpnessSetting = 0x506;
+    public static final int TagColorSpace = 0x507;
+    public static final int TagSceneMode = 0x509;
+    public static final int TagNoiseReduction = 0x50a;
+    public static final int TagDistortionCorrection = 0x50b;
+    public static final int TagShadingCompensation = 0x50c;
+    public static final int TagCompressionFactor = 0x50d;
+    public static final int TagGradation = 0x50f;
+    public static final int TagPictureMode = 0x520;
+    public static final int TagPictureModeSaturation = 0x521;
+    public static final int TagPictureModeHue = 0x522;
+    public static final int TagPictureModeContrast = 0x523;
+    public static final int TagPictureModeSharpness = 0x524;
+    public static final int TagPictureModeBWFilter = 0x525;
+    public static final int TagPictureModeTone = 0x526;
+    public static final int TagNoiseFilter = 0x527;
+    public static final int TagArtFilter = 0x529;
+    public static final int TagMagicFilter = 0x52c;
+    public static final int TagPictureModeEffect = 0x52d;
+    public static final int TagToneLevel = 0x52e;
+    public static final int TagArtFilterEffect = 0x52f;
+
+    public static final int TagDriveMode = 0x600;
+    public static final int TagPanoramaMode = 0x601;
+    public static final int TagImageQuality2 = 0x603;
+    public static final int TagImageStabilization = 0x604;
+
+    public static final int TagStackedImage = 0x804;
+
+    public static final int TagManometerPressure = 0x900;
+    public static final int TagManometerReading = 0x901;
+    public static final int TagExtendedWBDetect = 0x902;
+    public static final int TagRollAngle = 0x903;
+    public static final int TagPitchAngle = 0x904;
+    public static final int TagDateTimeUtc = 0x908;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagCameraSettingsVersion, "Camera Settings Version");
+        _tagNameMap.put(TagPreviewImageValid, "Preview Image Valid");
+        _tagNameMap.put(TagPreviewImageStart, "Preview Image Start");
+        _tagNameMap.put(TagPreviewImageLength, "Preview Image Length");
+
+        _tagNameMap.put(TagExposureMode, "Exposure Mode");
+        _tagNameMap.put(TagAeLock, "AE Lock");
+        _tagNameMap.put(TagMeteringMode, "Metering Mode");
+        _tagNameMap.put(TagExposureShift, "Exposure Shift");
+        _tagNameMap.put(TagNdFilter, "ND Filter");
+
+        _tagNameMap.put(TagMacroMode, "Macro Mode");
+        _tagNameMap.put(TagFocusMode, "Focus Mode");
+        _tagNameMap.put(TagFocusProcess, "Focus Process");
+        _tagNameMap.put(TagAfSearch, "AF Search");
+        _tagNameMap.put(TagAfAreas, "AF Areas");
+        _tagNameMap.put(TagAfPointSelected, "AF Point Selected");
+        _tagNameMap.put(TagAfFineTune, "AF Fine Tune");
+        _tagNameMap.put(TagAfFineTuneAdj, "AF Fine Tune Adj");
+
+        _tagNameMap.put(TagFlashMode, "Flash Mode");
+        _tagNameMap.put(TagFlashExposureComp, "Flash Exposure Comp");
+        _tagNameMap.put(TagFlashRemoteControl, "Flash Remote Control");
+        _tagNameMap.put(TagFlashControlMode, "Flash Control Mode");
+        _tagNameMap.put(TagFlashIntensity, "Flash Intensity");
+        _tagNameMap.put(TagManualFlashStrength, "Manual Flash Strength");
+
+        _tagNameMap.put(TagWhiteBalance2, "White Balance 2");
+        _tagNameMap.put(TagWhiteBalanceTemperature, "White Balance Temperature");
+        _tagNameMap.put(TagWhiteBalanceBracket, "White Balance Bracket");
+        _tagNameMap.put(TagCustomSaturation, "Custom Saturation");
+        _tagNameMap.put(TagModifiedSaturation, "Modified Saturation");
+        _tagNameMap.put(TagContrastSetting, "Contrast Setting");
+        _tagNameMap.put(TagSharpnessSetting, "Sharpness Setting");
+        _tagNameMap.put(TagColorSpace, "Color Space");
+        _tagNameMap.put(TagSceneMode, "Scene Mode");
+        _tagNameMap.put(TagNoiseReduction, "Noise Reduction");
+        _tagNameMap.put(TagDistortionCorrection, "Distortion Correction");
+        _tagNameMap.put(TagShadingCompensation, "Shading Compensation");
+        _tagNameMap.put(TagCompressionFactor, "Compression Factor");
+        _tagNameMap.put(TagGradation, "Gradation");
+        _tagNameMap.put(TagPictureMode, "Picture Mode");
+        _tagNameMap.put(TagPictureModeSaturation, "Picture Mode Saturation");
+        _tagNameMap.put(TagPictureModeHue, "Picture Mode Hue");
+        _tagNameMap.put(TagPictureModeContrast, "Picture Mode Contrast");
+        _tagNameMap.put(TagPictureModeSharpness, "Picture Mode Sharpness");
+        _tagNameMap.put(TagPictureModeBWFilter, "Picture Mode BW Filter");
+        _tagNameMap.put(TagPictureModeTone, "Picture Mode Tone");
+        _tagNameMap.put(TagNoiseFilter, "Noise Filter");
+        _tagNameMap.put(TagArtFilter, "Art Filter");
+        _tagNameMap.put(TagMagicFilter, "Magic Filter");
+        _tagNameMap.put(TagPictureModeEffect, "Picture Mode Effect");
+        _tagNameMap.put(TagToneLevel, "Tone Level");
+        _tagNameMap.put(TagArtFilterEffect, "Art Filter Effect");
+
+        _tagNameMap.put(TagDriveMode, "Drive Mode");
+        _tagNameMap.put(TagPanoramaMode, "Panorama Mode");
+        _tagNameMap.put(TagImageQuality2, "Image Quality 2");
+        _tagNameMap.put(TagImageStabilization, "Image Stabilization");
+
+        _tagNameMap.put(TagStackedImage, "Stacked Image");
+
+        _tagNameMap.put(TagManometerPressure, "Manometer Pressure");
+        _tagNameMap.put(TagManometerReading, "Manometer Reading");
+        _tagNameMap.put(TagExtendedWBDetect, "Extended WB Detect");
+        _tagNameMap.put(TagRollAngle, "Roll Angle");
+        _tagNameMap.put(TagPitchAngle, "Pitch Angle");
+        _tagNameMap.put(TagDateTimeUtc, "Date Time UTC");
+    }
+
+    public OlympusCameraSettingsMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusCameraSettingsMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Camera Settings";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java	(revision 10862)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java	(revision 10862)
@@ -0,0 +1,370 @@
+/*
+ * 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.text.DecimalFormat;
+import java.util.HashMap;
+
+import static com.drew.metadata.exif.makernotes.OlympusEquipmentMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusEquipmentMakernoteDirectory}.
+ * <p>
+ * Some Description functions and the Extender and Lens types lists converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusEquipmentMakernoteDescriptor extends TagDescriptor<OlympusEquipmentMakernoteDirectory>
+{
+    public OlympusEquipmentMakernoteDescriptor(@NotNull OlympusEquipmentMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_EQUIPMENT_VERSION:
+                return GetEquipmentVersionDescription();
+            case TAG_FOCAL_PLANE_DIAGONAL:
+                return GetFocalPlaneDiagonalDescription();
+            case TAG_BODY_FIRMWARE_VERSION:
+                return GetBodyFirmwareVersionDescription();
+            case TAG_LENS_TYPE:
+                return GetLensTypeDescription();
+            case TAG_LENS_FIRMWARE_VERSION:
+                return GetLensFirmwareVersionDescription();
+            case TAG_MAX_APERTURE_AT_MIN_FOCAL:
+                return GetMaxApertureAtMinFocalDescription();
+            case TAG_MAX_APERTURE_AT_MAX_FOCAL:
+                return GetMaxApertureAtMaxFocalDescription();
+            case TAG_MAX_APERTURE:
+                return GetMaxApertureDescription();
+            case TAG_LENS_PROPERTIES:
+                return GetLensPropertiesDescription();
+            case TAG_EXTENDER:
+                return GetExtenderDescription();
+            case TAG_FLASH_TYPE:
+                return GetFlashTypeDescription();
+            case TAG_FLASH_MODEL:
+                return GetFlashModelDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String GetEquipmentVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EQUIPMENT_VERSION, 4);
+    }
+
+    @Nullable
+    public String GetFocalPlaneDiagonalDescription()
+    {
+        return _directory.getString(TAG_FOCAL_PLANE_DIAGONAL) + " mm";
+    }
+
+    @Nullable
+    public String GetBodyFirmwareVersionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_BODY_FIRMWARE_VERSION);
+        if (value == null)
+            return null;
+
+        String hex = String.format("%04X", value);
+        return String.format("%s.%s",
+            hex.substring(0, hex.length() - 3),
+            hex.substring(hex.length() - 3));
+    }
+
+    @Nullable
+    public String GetLensTypeDescription()
+    {
+        String str = _directory.getString(TAG_LENS_TYPE);
+
+        if (str == null)
+            return null;
+
+        // The String contains six numbers:
+        //
+        // - Make
+        // - Unknown
+        // - Model
+        // - Sub-model
+        // - Unknown
+        // - Unknown
+        //
+        // Only the Make, Model and Sub-model are used to identify the lens type
+        String[] values = str.split(" ");
+
+        if (values.length < 6)
+            return null;
+
+        try {
+            int num1 = Integer.parseInt(values[0]);
+            int num2 = Integer.parseInt(values[2]);
+            int num3 = Integer.parseInt(values[3]);
+            return _olympusLensTypes.get(String.format("%X %02X %02X", num1, num2, num3));
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String GetLensFirmwareVersionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_LENS_FIRMWARE_VERSION);
+        if (value == null)
+            return null;
+
+        String hex = String.format("%04X", value);
+        return String.format("%s.%s",
+            hex.substring(0, hex.length() - 3),
+            hex.substring(hex.length() - 3));
+    }
+
+    @Nullable
+    public String GetMaxApertureAtMinFocalDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MIN_FOCAL);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        return format.format(CalcMaxAperture(value));
+    }
+
+    @Nullable
+    public String GetMaxApertureAtMaxFocalDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MAX_FOCAL);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        return format.format(CalcMaxAperture(value));
+    }
+
+    @Nullable
+    public String GetMaxApertureDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MAX_APERTURE);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        return format.format(CalcMaxAperture(value));
+    }
+
+    private static double CalcMaxAperture(int value)
+    {
+        return Math.pow(Math.sqrt(2.00), value / 256.0);
+    }
+
+    @Nullable
+    public String GetLensPropertiesDescription()
+    {
+        Integer value = _directory.getInteger(TAG_LENS_PROPERTIES);
+        if (value == null)
+            return null;
+
+        return String.format("0x%04X", value);
+    }
+
+    @Nullable
+    public String GetExtenderDescription()
+    {
+        String str = _directory.getString(TAG_EXTENDER);
+
+        if (str == null)
+            return null;
+
+        // The String contains six numbers:
+        //
+        // - Make
+        // - Unknown
+        // - Model
+        // - Sub-model
+        // - Unknown
+        // - Unknown
+        //
+        // Only the Make and Model are used to identify the extender
+        String[] values = str.split(" ");
+
+        if (values.length < 6)
+            return null;
+
+        try {
+            int num1 = Integer.parseInt(values[0]);
+            int num2 = Integer.parseInt(values[2]);
+            String extenderType = String.format("%X %02X", num1, num2);
+            return _olympusExtenderTypes.get(extenderType);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String GetFlashTypeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_TYPE,
+            "None", null, "Simple E-System", "E-System");
+    }
+
+    @Nullable
+    public String GetFlashModelDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODEL,
+            "None", "FL-20", "FL-50", "RF-11", "TF-22", "FL-36", "FL-50R", "FL-36R");
+    }
+
+    private static final HashMap<String, String> _olympusLensTypes = new HashMap<String, String>();
+    private static final HashMap<String, String> _olympusExtenderTypes = new HashMap<String, String>();
+
+    static {
+        _olympusLensTypes.put("0 00 00", "None");
+        // Olympus lenses (also Kenko Tokina)
+        _olympusLensTypes.put("0 01 00", "Olympus Zuiko Digital ED 50mm F2.0 Macro");
+        _olympusLensTypes.put("0 01 01", "Olympus Zuiko Digital 40-150mm F3.5-4.5"); //8
+        _olympusLensTypes.put("0 01 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6"); //PH (E-P1 pre-production)
+        _olympusLensTypes.put("0 02 00", "Olympus Zuiko Digital ED 150mm F2.0");
+        _olympusLensTypes.put("0 02 10", "Olympus M.Zuiko Digital 17mm F2.8 Pancake"); //PH (E-P1 pre-production)
+        _olympusLensTypes.put("0 03 00", "Olympus Zuiko Digital ED 300mm F2.8");
+        _olympusLensTypes.put("0 03 10", "Olympus M.Zuiko Digital ED 14-150mm F4.0-5.6 [II]"); //11 (The second version of this lens seems to have the same lens ID number as the first version #20)
+        _olympusLensTypes.put("0 04 10", "Olympus M.Zuiko Digital ED 9-18mm F4.0-5.6"); //11
+        _olympusLensTypes.put("0 05 00", "Olympus Zuiko Digital 14-54mm F2.8-3.5");
+        _olympusLensTypes.put("0 05 01", "Olympus Zuiko Digital Pro ED 90-250mm F2.8"); //9
+        _olympusLensTypes.put("0 05 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6 L"); //11 (E-PL1)
+        _olympusLensTypes.put("0 06 00", "Olympus Zuiko Digital ED 50-200mm F2.8-3.5");
+        _olympusLensTypes.put("0 06 01", "Olympus Zuiko Digital ED 8mm F3.5 Fisheye"); //9
+        _olympusLensTypes.put("0 06 10", "Olympus M.Zuiko Digital ED 40-150mm F4.0-5.6"); //PH
+        _olympusLensTypes.put("0 07 00", "Olympus Zuiko Digital 11-22mm F2.8-3.5");
+        _olympusLensTypes.put("0 07 01", "Olympus Zuiko Digital 18-180mm F3.5-6.3"); //6
+        _olympusLensTypes.put("0 07 10", "Olympus M.Zuiko Digital ED 12mm F2.0"); //PH
+        _olympusLensTypes.put("0 08 01", "Olympus Zuiko Digital 70-300mm F4.0-5.6"); //7 (seen as release 1 - PH)
+        _olympusLensTypes.put("0 08 10", "Olympus M.Zuiko Digital ED 75-300mm F4.8-6.7"); //PH
+        _olympusLensTypes.put("0 09 10", "Olympus M.Zuiko Digital 14-42mm F3.5-5.6 II"); //PH (E-PL2)
+        _olympusLensTypes.put("0 10 01", "Kenko Tokina Reflex 300mm F6.3 MF Macro"); //20
+        _olympusLensTypes.put("0 10 10", "Olympus M.Zuiko Digital ED 12-50mm F3.5-6.3 EZ"); //PH
+        _olympusLensTypes.put("0 11 10", "Olympus M.Zuiko Digital 45mm F1.8"); //17
+        _olympusLensTypes.put("0 12 10", "Olympus M.Zuiko Digital ED 60mm F2.8 Macro"); //20
+        _olympusLensTypes.put("0 13 10", "Olympus M.Zuiko Digital 14-42mm F3.5-5.6 II R"); //PH/20
+        _olympusLensTypes.put("0 14 10", "Olympus M.Zuiko Digital ED 40-150mm F4.0-5.6 R"); //19
+        // '0 14 10.1", "Olympus M.Zuiko Digital ED 14-150mm F4.0-5.6 II"); //11 (questionable & unconfirmed -- all samples I can find are '0 3 10' - PH)
+        _olympusLensTypes.put("0 15 00", "Olympus Zuiko Digital ED 7-14mm F4.0");
+        _olympusLensTypes.put("0 15 10", "Olympus M.Zuiko Digital ED 75mm F1.8"); //PH
+        _olympusLensTypes.put("0 16 10", "Olympus M.Zuiko Digital 17mm F1.8"); //20
+        _olympusLensTypes.put("0 17 00", "Olympus Zuiko Digital Pro ED 35-100mm F2.0"); //7
+        _olympusLensTypes.put("0 18 00", "Olympus Zuiko Digital 14-45mm F3.5-5.6");
+        _olympusLensTypes.put("0 18 10", "Olympus M.Zuiko Digital ED 75-300mm F4.8-6.7 II"); //20
+        _olympusLensTypes.put("0 19 10", "Olympus M.Zuiko Digital ED 12-40mm F2.8 Pro"); //PH
+        _olympusLensTypes.put("0 20 00", "Olympus Zuiko Digital 35mm F3.5 Macro"); //9
+        _olympusLensTypes.put("0 20 10", "Olympus M.Zuiko Digital ED 40-150mm F2.8 Pro"); //20
+        _olympusLensTypes.put("0 21 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6 EZ"); //20
+        _olympusLensTypes.put("0 22 00", "Olympus Zuiko Digital 17.5-45mm F3.5-5.6"); //9
+        _olympusLensTypes.put("0 22 10", "Olympus M.Zuiko Digital 25mm F1.8"); //20
+        _olympusLensTypes.put("0 23 00", "Olympus Zuiko Digital ED 14-42mm F3.5-5.6"); //PH
+        _olympusLensTypes.put("0 23 10", "Olympus M.Zuiko Digital ED 7-14mm F2.8 Pro"); //20
+        _olympusLensTypes.put("0 24 00", "Olympus Zuiko Digital ED 40-150mm F4.0-5.6"); //PH
+        _olympusLensTypes.put("0 24 10", "Olympus M.Zuiko Digital ED 300mm F4.0 IS Pro"); //20
+        _olympusLensTypes.put("0 25 10", "Olympus M.Zuiko Digital ED 8mm F1.8 Fisheye Pro"); //20
+        _olympusLensTypes.put("0 30 00", "Olympus Zuiko Digital ED 50-200mm F2.8-3.5 SWD"); //7
+        _olympusLensTypes.put("0 31 00", "Olympus Zuiko Digital ED 12-60mm F2.8-4.0 SWD"); //7
+        _olympusLensTypes.put("0 32 00", "Olympus Zuiko Digital ED 14-35mm F2.0 SWD"); //PH
+        _olympusLensTypes.put("0 33 00", "Olympus Zuiko Digital 25mm F2.8"); //PH
+        _olympusLensTypes.put("0 34 00", "Olympus Zuiko Digital ED 9-18mm F4.0-5.6"); //7
+        _olympusLensTypes.put("0 35 00", "Olympus Zuiko Digital 14-54mm F2.8-3.5 II"); //PH
+        // Sigma lenses
+        _olympusLensTypes.put("1 01 00", "Sigma 18-50mm F3.5-5.6 DC"); //8
+        _olympusLensTypes.put("1 01 10", "Sigma 30mm F2.8 EX DN"); //20
+        _olympusLensTypes.put("1 02 00", "Sigma 55-200mm F4.0-5.6 DC");
+        _olympusLensTypes.put("1 02 10", "Sigma 19mm F2.8 EX DN"); //20
+        _olympusLensTypes.put("1 03 00", "Sigma 18-125mm F3.5-5.6 DC");
+        _olympusLensTypes.put("1 03 10", "Sigma 30mm F2.8 DN | A"); //20
+        _olympusLensTypes.put("1 04 00", "Sigma 18-125mm F3.5-5.6 DC"); //7
+        _olympusLensTypes.put("1 04 10", "Sigma 19mm F2.8 DN | A"); //20
+        _olympusLensTypes.put("1 05 00", "Sigma 30mm F1.4 EX DC HSM"); //10
+        _olympusLensTypes.put("1 05 10", "Sigma 60mm F2.8 DN | A"); //20
+        _olympusLensTypes.put("1 06 00", "Sigma APO 50-500mm F4.0-6.3 EX DG HSM"); //6
+        _olympusLensTypes.put("1 07 00", "Sigma Macro 105mm F2.8 EX DG"); //PH
+        _olympusLensTypes.put("1 08 00", "Sigma APO Macro 150mm F2.8 EX DG HSM"); //PH
+        _olympusLensTypes.put("1 09 00", "Sigma 18-50mm F2.8 EX DC Macro"); //20
+        _olympusLensTypes.put("1 10 00", "Sigma 24mm F1.8 EX DG Aspherical Macro"); //PH
+        _olympusLensTypes.put("1 11 00", "Sigma APO 135-400mm F4.5-5.6 DG"); //11
+        _olympusLensTypes.put("1 12 00", "Sigma APO 300-800mm F5.6 EX DG HSM"); //11
+        _olympusLensTypes.put("1 13 00", "Sigma 30mm F1.4 EX DC HSM"); //11
+        _olympusLensTypes.put("1 14 00", "Sigma APO 50-500mm F4.0-6.3 EX DG HSM"); //11
+        _olympusLensTypes.put("1 15 00", "Sigma 10-20mm F4.0-5.6 EX DC HSM"); //11
+        _olympusLensTypes.put("1 16 00", "Sigma APO 70-200mm F2.8 II EX DG Macro HSM"); //11
+        _olympusLensTypes.put("1 17 00", "Sigma 50mm F1.4 EX DG HSM"); //11
+        // Panasonic/Leica lenses
+        _olympusLensTypes.put("2 01 00", "Leica D Vario Elmarit 14-50mm F2.8-3.5 Asph."); //11
+        _olympusLensTypes.put("2 01 10", "Lumix G Vario 14-45mm F3.5-5.6 Asph. Mega OIS"); //16
+        _olympusLensTypes.put("2 02 00", "Leica D Summilux 25mm F1.4 Asph."); //11
+        _olympusLensTypes.put("2 02 10", "Lumix G Vario 45-200mm F4.0-5.6 Mega OIS"); //16
+        _olympusLensTypes.put("2 03 00", "Leica D Vario Elmar 14-50mm F3.8-5.6 Asph. Mega OIS"); //11
+        _olympusLensTypes.put("2 03 01", "Leica D Vario Elmar 14-50mm F3.8-5.6 Asph."); //14 (L10 kit)
+        _olympusLensTypes.put("2 03 10", "Lumix G Vario HD 14-140mm F4.0-5.8 Asph. Mega OIS"); //16
+        _olympusLensTypes.put("2 04 00", "Leica D Vario Elmar 14-150mm F3.5-5.6"); //13
+        _olympusLensTypes.put("2 04 10", "Lumix G Vario 7-14mm F4.0 Asph."); //PH (E-P1 pre-production)
+        _olympusLensTypes.put("2 05 10", "Lumix G 20mm F1.7 Asph."); //16
+        _olympusLensTypes.put("2 06 10", "Leica DG Macro-Elmarit 45mm F2.8 Asph. Mega OIS"); //PH
+        _olympusLensTypes.put("2 07 10", "Lumix G Vario 14-42mm F3.5-5.6 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 08 10", "Lumix G Fisheye 8mm F3.5"); //PH
+        _olympusLensTypes.put("2 09 10", "Lumix G Vario 100-300mm F4.0-5.6 Mega OIS"); //11
+        _olympusLensTypes.put("2 10 10", "Lumix G 14mm F2.5 Asph."); //17
+        _olympusLensTypes.put("2 11 10", "Lumix G 12.5mm F12 3D"); //20 (H-FT012)
+        _olympusLensTypes.put("2 12 10", "Leica DG Summilux 25mm F1.4 Asph."); //20
+        _olympusLensTypes.put("2 13 10", "Lumix G X Vario PZ 45-175mm F4.0-5.6 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 14 10", "Lumix G X Vario PZ 14-42mm F3.5-5.6 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 15 10", "Lumix G X Vario 12-35mm F2.8 Asph. Power OIS"); //PH
+        _olympusLensTypes.put("2 16 10", "Lumix G Vario 45-150mm F4.0-5.6 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 17 10", "Lumix G X Vario 35-100mm F2.8 Power OIS"); //PH
+        _olympusLensTypes.put("2 18 10", "Lumix G Vario 14-42mm F3.5-5.6 II Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 19 10", "Lumix G Vario 14-140mm F3.5-5.6 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 20 10", "Lumix G Vario 12-32mm F3.5-5.6 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 21 10", "Leica DG Nocticron 42.5mm F1.2 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 22 10", "Leica DG Summilux 15mm F1.7 Asph."); //20
+        // '2 23 10", "Lumix G Vario 35-100mm F4.0-5.6 Asph. Mega OIS"); //20 (guess)
+        _olympusLensTypes.put("2 24 10", "Lumix G Macro 30mm F2.8 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 25 10", "Lumix G 42.5mm F1.7 Asph. Power OIS"); //20
+        _olympusLensTypes.put("3 01 00", "Leica D Vario Elmarit 14-50mm F2.8-3.5 Asph."); //11
+        _olympusLensTypes.put("3 02 00", "Leica D Summilux 25mm F1.4 Asph."); //11
+        // Tamron lenses
+        _olympusLensTypes.put("5 01 10", "Tamron 14-150mm F3.5-5.8 Di III"); //20 (model C001)
+
+
+        _olympusExtenderTypes.put("0 00", "None");
+        _olympusExtenderTypes.put("0 04", "Olympus Zuiko Digital EC-14 1.4x Teleconverter");
+        _olympusExtenderTypes.put("0 08", "Olympus EX-25 Extension Tube");
+        _olympusExtenderTypes.put("0 10", "Olympus Zuiko Digital EC-20 2.0x Teleconverter");
+    }
+}
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java	(revision 10862)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java	(revision 10862)
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+/**
+ * The Olympus equipment makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusEquipmentMakernoteDirectory extends Directory
+{
+    public static final int TAG_EQUIPMENT_VERSION = 0x0000;
+    public static final int TAG_CAMERA_TYPE_2 = 0x0100;
+    public static final int TAG_SERIAL_NUMBER = 0x0101;
+
+    public static final int TAG_INTERNAL_SERIAL_NUMBER = 0x0102;
+    public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0103;
+    public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104;
+
+    public static final int TAG_LENS_TYPE = 0x0201;
+    public static final int TAG_LENS_SERIAL_NUMBER = 0x0202;
+    public static final int TAG_LENS_MODEL = 0x0203;
+    public static final int TAG_LENS_FIRMWARE_VERSION = 0x0204;
+    public static final int TAG_MAX_APERTURE_AT_MIN_FOCAL = 0x0205;
+    public static final int TAG_MAX_APERTURE_AT_MAX_FOCAL = 0x0206;
+    public static final int TAG_MIN_FOCAL_LENGTH = 0x0207;
+    public static final int TAG_MAX_FOCAL_LENGTH = 0x0208;
+    public static final int TAG_MAX_APERTURE = 0x020A;
+    public static final int TAG_LENS_PROPERTIES = 0x020B;
+
+    public static final int TAG_EXTENDER = 0x0301;
+    public static final int TAG_EXTENDER_SERIAL_NUMBER = 0x0302;
+    public static final int TAG_EXTENDER_MODEL = 0x0303;
+    public static final int TAG_EXTENDER_FIRMWARE_VERSION = 0x0304;
+
+    public static final int TAG_CONVERSION_LENS = 0x0403;
+
+    public static final int TAG_FLASH_TYPE = 0x1000;
+    public static final int TAG_FLASH_MODEL = 0x1001;
+    public static final int TAG_FLASH_FIRMWARE_VERSION = 0x1002;
+    public static final int TAG_FLASH_SERIAL_NUMBER = 0x1003;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_EQUIPMENT_VERSION, "Equipment Version");
+        _tagNameMap.put(TAG_CAMERA_TYPE_2, "Camera Type 2");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_INTERNAL_SERIAL_NUMBER, "Internal Serial Number");
+        _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
+        _tagNameMap.put(TAG_BODY_FIRMWARE_VERSION, "Body Firmware Version");
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        _tagNameMap.put(TAG_LENS_MODEL, "Lens Model");
+        _tagNameMap.put(TAG_LENS_FIRMWARE_VERSION, "Lens Firmware Version");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MIN_FOCAL, "Max Aperture At Min Focal");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MAX_FOCAL, "Max Aperture At Max Focal");
+        _tagNameMap.put(TAG_MIN_FOCAL_LENGTH, "Min Focal Length");
+        _tagNameMap.put(TAG_MAX_FOCAL_LENGTH, "Max Focal Length");
+        _tagNameMap.put(TAG_MAX_APERTURE, "Max Aperture");
+        _tagNameMap.put(TAG_LENS_PROPERTIES, "Lens Properties");
+        _tagNameMap.put(TAG_EXTENDER, "Extender");
+        _tagNameMap.put(TAG_EXTENDER_SERIAL_NUMBER, "Extender Serial Number");
+        _tagNameMap.put(TAG_EXTENDER_MODEL, "Extender Model");
+        _tagNameMap.put(TAG_EXTENDER_FIRMWARE_VERSION, "Extender Firmware Version");
+        _tagNameMap.put(TAG_CONVERSION_LENS, "Conversion Lens");
+        _tagNameMap.put(TAG_FLASH_TYPE, "Flash Type");
+        _tagNameMap.put(TAG_FLASH_MODEL, "Flash Model");
+        _tagNameMap.put(TAG_FLASH_FIRMWARE_VERSION, "Flash Firmware Version");
+        _tagNameMap.put(TAG_FLASH_SERIAL_NUMBER, "Flash Serial Number");
+    }
+
+    public OlympusEquipmentMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusEquipmentMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Equipment";
+    }
+
+    @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 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,9 +21,11 @@
 package com.drew.metadata.exif.makernotes;
 
+import com.drew.lang.DateUtil;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
-import java.util.GregorianCalendar;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
 
 import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*;
@@ -115,5 +117,5 @@
                 return getFocusDistanceDescription();
             case CameraSettings.TAG_FLASH_FIRED:
-                return getFlastFiredDescription();
+                return getFlashFiredDescription();
             case CameraSettings.TAG_DATE:
                 return getDateDescription();
@@ -142,5 +144,5 @@
                 return getSubjectProgramDescription();
             case CameraSettings.TAG_FLASH_COMPENSATION:
-                return getFlastCompensationDescription();
+                return getFlashCompensationDescription();
             case CameraSettings.TAG_ISO_SETTING:
                 return getIsoSettingDescription();
@@ -258,5 +260,7 @@
 
         double iso = Math.pow((value / 8d) - 1, 2) * 3.125;
-        return Double.toString(iso);
+        DecimalFormat format = new DecimalFormat("0.##");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return format.format(iso);
     }
 
@@ -274,5 +278,7 @@
 
         double shutterSpeed = Math.pow((49-value) / 8d, 2);
-        return Double.toString(shutterSpeed) + " sec";
+        DecimalFormat format = new DecimalFormat("0.###");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return format.format(shutterSpeed) + " sec";
     }
 
@@ -289,5 +295,5 @@
 
         double fStop = Math.pow((value/16d) - 0.5, 2);
-        return "F" + Double.toString(fStop);
+        return getFStopDescription(fStop);
     }
 
@@ -308,5 +314,6 @@
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_EXPOSURE_COMPENSATION);
-        return value == null ? null : ((value / 3d) - 2) + " EV";
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format((value / 3d) - 2) + " EV";
     }
 
@@ -341,5 +348,5 @@
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_FOCAL_LENGTH);
-        return value == null ? null : Double.toString(value/256d) + " mm";
+        return value == null ? null : getFocalLengthDescription(value/256d);
     }
 
@@ -356,5 +363,5 @@
 
     @Nullable
-    public String getFlastFiredDescription()
+    public String getFlashFiredDescription()
     {
         return getIndexedDescription(CameraSettings.TAG_FLASH_FIRED, "No", "Yes");
@@ -370,8 +377,13 @@
         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();
+
+        int day = (int) (value & 0xFF);
+        int month = (int) ((value >> 16) & 0xFF);
+        int year = (int) ((value >> 8) & 0xFF) + 1970;
+
+        if (!DateUtil.isValidDate(year, month, day))
+            return "Invalid date";
+
+        return String.format("%04d-%02d-%02d", year, month + 1, day);
     }
 
@@ -385,7 +397,11 @@
         if (value == null)
             return null;
-        long hours = (value >> 8) & 0xFF;
-        long minutes = (value >> 16) & 0xFF;
-        long seconds = value & 0xFF;
+
+        int hours = (int) ((value >> 8) & 0xFF);
+        int minutes = (int) ((value >> 16) & 0xFF);
+        int seconds = (int) (value & 0xFF);
+
+        if (!DateUtil.isValidTime(hours, minutes, seconds))
+            return "Invalid time";
 
         return String.format("%02d:%02d:%02d", hours, minutes, seconds);
@@ -400,5 +416,5 @@
             return null;
         double fStop = Math.pow((value/16d) - 0.5, 2);
-        return "F" + fStop;
+        return getFStopDescription(fStop);
     }
 
@@ -424,5 +440,6 @@
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_RED);
-        return value == null ? null : Double.toString(value/256d);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format(value/256d);
     }
 
@@ -431,5 +448,6 @@
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_GREEN);
-        return value == null ? null : Double.toString(value/256d);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format(value/256d);
     }
 
@@ -438,5 +456,6 @@
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_BLUE);
-        return value == null ? null : Double.toString(value/256d);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format(value / 256d);
     }
 
@@ -468,8 +487,9 @@
 
     @Nullable
-    public String getFlastCompensationDescription()
+    public String getFlashCompensationDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_FLASH_COMPENSATION);
-        return value == null ? null : ((value-6)/3d) + " EV";
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format((value-6)/3d) + " EV";
     }
 
@@ -535,5 +555,6 @@
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE);
-        return value == null ? null : Double.toString((value/8d)-6);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format((value/8d)-6);
     }
 
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/package.html
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/makernotes/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/package.html
===================================================================
--- trunk/src/com/drew/metadata/exif/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/exif/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,4 +25,6 @@
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.file.FileMetadataDirectory.*;
+
 /**
  * @author Drew Noakes https://drewnoakes.com
@@ -40,5 +42,5 @@
     {
         switch (tagType) {
-            case FileMetadataDirectory.TAG_FILE_SIZE:
+            case TAG_FILE_SIZE:
                 return getFileSizeDescription();
             default:
@@ -50,5 +52,5 @@
     private String getFileSizeDescription()
     {
-        Long size = _directory.getLongObject(FileMetadataDirectory.TAG_FILE_SIZE);
+        Long size = _directory.getLongObject(TAG_FILE_SIZE);
 
         if (size == null)
Index: trunk/src/com/drew/metadata/file/FileMetadataDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/file/FileMetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 10861)
+++ trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 10862)
@@ -1,2 +1,22 @@
+/*
+ * Copyright 2002-2016 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.file;
 
Index: trunk/src/com/drew/metadata/file/package.html
===================================================================
--- trunk/src/com/drew/metadata/file/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/file/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/iptc/IptcDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,4 +26,6 @@
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.iptc.IptcDirectory.*;
+
 /**
  * Provides human-readable string representations of tag values stored in a {@link IptcDirectory}.
@@ -45,12 +47,30 @@
     {
         switch (tagType) {
-            case IptcDirectory.TAG_FILE_FORMAT:
+            case TAG_DATE_CREATED:
+                return getDateCreatedDescription();
+            case TAG_DIGITAL_DATE_CREATED:
+                return getDigitalDateCreatedDescription();
+            case TAG_DATE_SENT:
+                return getDateSentDescription();
+            case TAG_EXPIRATION_DATE:
+                return getExpirationDateDescription();
+            case TAG_EXPIRATION_TIME:
+                return getExpirationTimeDescription();
+            case TAG_FILE_FORMAT:
                 return getFileFormatDescription();
-            case IptcDirectory.TAG_KEYWORDS:
+            case TAG_KEYWORDS:
                 return getKeywordsDescription();
-            case IptcDirectory.TAG_TIME_CREATED:
+            case TAG_REFERENCE_DATE:
+                return getReferenceDateDescription();
+            case TAG_RELEASE_DATE:
+                return getReleaseDateDescription();
+            case TAG_RELEASE_TIME:
+                return getReleaseTimeDescription();
+            case TAG_TIME_CREATED:
                 return getTimeCreatedDescription();
-            case IptcDirectory.TAG_DIGITAL_TIME_CREATED:
+            case TAG_DIGITAL_TIME_CREATED:
                 return getDigitalTimeCreatedDescription();
+            case TAG_TIME_SENT:
+                return getTimeSentDescription();
             default:
                 return super.getDescription(tagType);
@@ -59,7 +79,29 @@
 
     @Nullable
+    public String getDateDescription(int tagType)
+    {
+        String s = _directory.getString(tagType);
+        if (s == null)
+            return null;
+        if (s.length() == 8)
+            return s.substring(0, 4) + ':' + s.substring(4, 6) + ':' + s.substring(6);
+        return s;
+    }
+
+    @Nullable
+    public String getTimeDescription(int tagType)
+    {
+        String s = _directory.getString(tagType);
+        if (s == null)
+            return null;
+        if (s.length() == 6 || s.length() == 11)
+            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
+        return s;
+    }
+
+    @Nullable
     public String getFileFormatDescription()
     {
-        Integer value = _directory.getInteger(IptcDirectory.TAG_FILE_FORMAT);
+        Integer value = _directory.getInteger(TAG_FILE_FORMAT);
         if (value == null)
             return null;
@@ -102,5 +144,5 @@
     public String getByLineDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_BY_LINE);
+        return _directory.getString(TAG_BY_LINE);
     }
 
@@ -108,5 +150,5 @@
     public String getByLineTitleDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_BY_LINE_TITLE);
+        return _directory.getString(TAG_BY_LINE_TITLE);
     }
 
@@ -114,5 +156,5 @@
     public String getCaptionDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CAPTION);
+        return _directory.getString(TAG_CAPTION);
     }
 
@@ -120,5 +162,5 @@
     public String getCategoryDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CATEGORY);
+        return _directory.getString(TAG_CATEGORY);
     }
 
@@ -126,5 +168,5 @@
     public String getCityDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CITY);
+        return _directory.getString(TAG_CITY);
     }
 
@@ -132,5 +174,5 @@
     public String getCopyrightNoticeDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_COPYRIGHT_NOTICE);
+        return _directory.getString(TAG_COPYRIGHT_NOTICE);
     }
 
@@ -138,5 +180,5 @@
     public String getCountryOrPrimaryLocationDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME);
+        return _directory.getString(TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME);
     }
 
@@ -144,5 +186,5 @@
     public String getCreditDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CREDIT);
+        return _directory.getString(TAG_CREDIT);
     }
 
@@ -150,5 +192,29 @@
     public String getDateCreatedDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_DATE_CREATED);
+        return getDateDescription(TAG_DATE_CREATED);
+    }
+
+    @Nullable
+    public String getDigitalDateCreatedDescription()
+    {
+        return getDateDescription(TAG_DIGITAL_DATE_CREATED);
+    }
+
+    @Nullable
+    public String getDateSentDescription()
+    {
+        return getDateDescription(TAG_DATE_SENT);
+    }
+
+    @Nullable
+    public String getExpirationDateDescription()
+    {
+        return getDateDescription(TAG_EXPIRATION_DATE);
+    }
+
+    @Nullable
+    public String getExpirationTimeDescription()
+    {
+        return getTimeDescription(TAG_EXPIRATION_TIME);
     }
 
@@ -156,5 +222,5 @@
     public String getHeadlineDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_HEADLINE);
+        return _directory.getString(TAG_HEADLINE);
     }
 
@@ -162,5 +228,5 @@
     public String getKeywordsDescription()
     {
-        final String[] keywords = _directory.getStringArray(IptcDirectory.TAG_KEYWORDS);
+        final String[] keywords = _directory.getStringArray(TAG_KEYWORDS);
         if (keywords==null)
             return null;
@@ -171,5 +237,5 @@
     public String getObjectNameDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_OBJECT_NAME);
+        return _directory.getString(TAG_OBJECT_NAME);
     }
 
@@ -177,5 +243,5 @@
     public String getOriginalTransmissionReferenceDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE);
+        return _directory.getString(TAG_ORIGINAL_TRANSMISSION_REFERENCE);
     }
 
@@ -183,5 +249,5 @@
     public String getOriginatingProgramDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_ORIGINATING_PROGRAM);
+        return _directory.getString(TAG_ORIGINATING_PROGRAM);
     }
 
@@ -189,5 +255,5 @@
     public String getProvinceOrStateDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_PROVINCE_OR_STATE);
+        return _directory.getString(TAG_PROVINCE_OR_STATE);
     }
 
@@ -195,5 +261,11 @@
     public String getRecordVersionDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_APPLICATION_RECORD_VERSION);
+        return _directory.getString(TAG_APPLICATION_RECORD_VERSION);
+    }
+
+    @Nullable
+    public String getReferenceDateDescription()
+    {
+        return getDateDescription(TAG_REFERENCE_DATE);
     }
 
@@ -201,5 +273,5 @@
     public String getReleaseDateDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_RELEASE_DATE);
+        return getDateDescription(TAG_RELEASE_DATE);
     }
 
@@ -207,5 +279,5 @@
     public String getReleaseTimeDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_RELEASE_TIME);
+        return getTimeDescription(TAG_RELEASE_TIME);
     }
 
@@ -213,5 +285,5 @@
     public String getSourceDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_SOURCE);
+        return _directory.getString(TAG_SOURCE);
     }
 
@@ -219,5 +291,5 @@
     public String getSpecialInstructionsDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS);
+        return _directory.getString(TAG_SPECIAL_INSTRUCTIONS);
     }
 
@@ -225,5 +297,5 @@
     public String getSupplementalCategoriesDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES);
+        return _directory.getString(TAG_SUPPLEMENTAL_CATEGORIES);
     }
 
@@ -231,10 +303,5 @@
     public String getTimeCreatedDescription()
     {
-        String s = _directory.getString(IptcDirectory.TAG_TIME_CREATED);
-        if (s == null)
-            return null;
-        if (s.length() == 6 || s.length() == 11)
-            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
-        return s;
+        return getTimeDescription(TAG_TIME_CREATED);
     }
 
@@ -242,10 +309,11 @@
     public String getDigitalTimeCreatedDescription()
     {
-        String s = _directory.getString(IptcDirectory.TAG_DIGITAL_TIME_CREATED);
-        if (s == null)
-            return null;
-        if (s.length() == 6 || s.length() == 11)
-            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
-        return s;
+        return getTimeDescription(TAG_DIGITAL_TIME_CREATED);
+    }
+
+    @Nullable
+    public String getTimeSentDescription()
+    {
+        return getTimeDescription(TAG_TIME_SENT);
     }
 
@@ -253,5 +321,5 @@
     public String getUrgencyDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_URGENCY);
+        return _directory.getString(TAG_URGENCY);
     }
 
@@ -259,5 +327,5 @@
     public String getWriterDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CAPTION_WRITER);
+        return _directory.getString(TAG_CAPTION_WRITER);
     }
 }
Index: trunk/src/com/drew/metadata/iptc/IptcDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,5 +25,9 @@
 import com.drew.metadata.Directory;
 
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -231,8 +235,83 @@
     public List<String> getKeywords()
     {
-        final String[] array = getStringArray(IptcDirectory.TAG_KEYWORDS);
+        final String[] array = getStringArray(TAG_KEYWORDS);
         if (array==null)
             return null;
         return Arrays.asList(array);
     }
+
+    /**
+     * Parses the Date Sent tag and the Time Sent tag to obtain a single Date object representing the
+     * date and time when the service sent this image.
+     * @return A Date object representing when the service sent this image, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateSent()
+    {
+        return getDate(TAG_DATE_SENT, TAG_TIME_SENT);
+    }
+
+    /**
+     * Parses the Release Date tag and the Release Time tag to obtain a single Date object representing the
+     * date and time when this image was released.
+     * @return A Date object representing when this image was released, if possible, otherwise null
+     */
+    @Nullable
+    public Date getReleaseDate()
+    {
+        return getDate(TAG_RELEASE_DATE, TAG_RELEASE_TIME);
+    }
+
+    /**
+     * Parses the Expiration Date tag and the Expiration Time tag to obtain a single Date object representing
+     * that this image should not used after this date and time.
+     * @return A Date object representing when this image was released, if possible, otherwise null
+     */
+    @Nullable
+    public Date getExpirationDate()
+    {
+        return getDate(TAG_EXPIRATION_DATE, TAG_EXPIRATION_TIME);
+    }
+
+    /**
+     * Parses the Date Created tag and the Time Created tag to obtain a single Date object representing the
+     * date and time when this image was captured.
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateCreated()
+    {
+        return getDate(TAG_DATE_CREATED, TAG_TIME_CREATED);
+    }
+
+    /**
+     * Parses the Digital Date Created tag and the Digital Time Created tag to obtain a single Date object
+     * representing the date and time when the digital representation of this image was created.
+     * @return A Date object representing when the digital representation of this image was created,
+     * if possible, otherwise null
+     */
+    @Nullable
+    public Date getDigitalDateCreated()
+    {
+        return getDate(TAG_DIGITAL_DATE_CREATED, TAG_DIGITAL_TIME_CREATED);
+    }
+
+    @Nullable
+    private Date getDate(int dateTagType, int timeTagType)
+    {
+        String date = getString(dateTagType);
+        String time = getString(timeTagType);
+
+        if (date == null)
+            return null;
+        if (time == null)
+            return null;
+
+        try {
+            DateFormat parser = new SimpleDateFormat("yyyyMMddHHmmssZ");
+            return parser.parse(date + time);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
 }
Index: trunk/src/com/drew/metadata/iptc/IptcReader.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 10861)
+++ trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,10 +26,10 @@
 import com.drew.lang.SequentialReader;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Date;
+import java.util.Collections;
 
 /**
@@ -60,5 +60,5 @@
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APPD);
+        return Collections.singletonList(JpegSegmentType.APPD);
     }
 
@@ -78,6 +78,17 @@
     public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length)
     {
+        extract(reader, metadata, length, null);
+    }
+
+    /**
+     * 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, @Nullable Directory parentDirectory)
+    {
         IptcDirectory directory = new IptcDirectory();
         metadata.addDirectory(directory);
+
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
 
         int offset = 0;
@@ -105,5 +116,5 @@
 
             // we need at least five bytes left to read a tag
-            if (offset + 5 >= length) {
+            if (offset + 5 > length) {
                 directory.addError("Too few bytes remain for a valid IPTC tag");
                 return;
@@ -184,25 +195,4 @@
                 reader.skip(tagByteCount - 1);
                 return;
-            case IptcDirectory.TAG_RELEASE_DATE:
-            case IptcDirectory.TAG_DATE_CREATED:
-                // Date object
-                if (tagByteCount >= 8) {
-                    string = reader.getString(tagByteCount);
-                    try {
-                        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 process the 'string' value below
-                    }
-                } else {
-                    reader.skip(tagByteCount);
-                }
-            case IptcDirectory.TAG_RELEASE_TIME:
-            case IptcDirectory.TAG_TIME_CREATED:
-                // time...
             default:
                 // fall through
@@ -227,4 +217,5 @@
             String[] newStrings;
             if (oldStrings == null) {
+                // TODO hitting this block means any prior value(s) are discarded
                 newStrings = new String[1];
             } else {
Index: trunk/src/com/drew/metadata/iptc/Iso2022Converter.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 10861)
+++ trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 10862)
@@ -1,2 +1,22 @@
+/*
+ * Copyright 2002-2016 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.iptc;
 
@@ -40,5 +60,5 @@
     /**
      * Attempts to guess the encoding of a string provided as a byte array.
-     * <p/>
+     * <p>
      * Encodings trialled are, in order:
      * <ul>
@@ -47,9 +67,9 @@
      *     <li>ISO-8859-1</li>
      * </ul>
-     * <p/>
+     * <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/>
+     * <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.
Index: trunk/src/com/drew/metadata/iptc/package.html
===================================================================
--- trunk/src/com/drew/metadata/iptc/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/iptc/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,5 +26,5 @@
 import com.drew.metadata.Metadata;
 
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
@@ -39,11 +39,5 @@
     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;
+        return Collections.singletonList(JpegSegmentType.COM);
     }
 
Index: trunk/src/com/drew/metadata/jpeg/JpegComponent.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,5 +21,5 @@
 package com.drew.metadata.jpeg;
 
-import com.drew.lang.annotations.Nullable;
+import com.drew.lang.annotations.NotNull;
 
 import java.io.Serializable;
@@ -55,5 +55,5 @@
      * @return the component name
      */
-    @Nullable
+    @NotNull
     public String getComponentName()
     {
@@ -70,6 +70,7 @@
             case 5:
                 return "Q";
+            default:
+                return String.format("Unknown (%s)", _componentId);
         }
-        return null;
     }
 
@@ -81,10 +82,10 @@
     public int getHorizontalSamplingFactor()
     {
-        return _samplingFactorByte & 0x0F;
+        return (_samplingFactorByte>>4) & 0x0F;
     }
 
     public int getVerticalSamplingFactor()
     {
-        return (_samplingFactorByte>>4) & 0x0F;
+        return _samplingFactorByte & 0x0F;
     }
 }
Index: trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,4 +25,6 @@
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.jpeg.JpegDirectory.*;
+
 /**
  * Provides human-readable string versions of the tags stored in a JpegDirectory.
@@ -44,19 +46,19 @@
         switch (tagType)
         {
-            case JpegDirectory.TAG_COMPRESSION_TYPE:
+            case TAG_COMPRESSION_TYPE:
                 return getImageCompressionTypeDescription();
-            case JpegDirectory.TAG_COMPONENT_DATA_1:
+            case TAG_COMPONENT_DATA_1:
                 return getComponentDataDescription(0);
-            case JpegDirectory.TAG_COMPONENT_DATA_2:
+            case TAG_COMPONENT_DATA_2:
                 return getComponentDataDescription(1);
-            case JpegDirectory.TAG_COMPONENT_DATA_3:
+            case TAG_COMPONENT_DATA_3:
                 return getComponentDataDescription(2);
-            case JpegDirectory.TAG_COMPONENT_DATA_4:
+            case TAG_COMPONENT_DATA_4:
                 return getComponentDataDescription(3);
-            case JpegDirectory.TAG_DATA_PRECISION:
+            case TAG_DATA_PRECISION:
                 return getDataPrecisionDescription();
-            case JpegDirectory.TAG_IMAGE_HEIGHT:
+            case TAG_IMAGE_HEIGHT:
                 return getImageHeightDescription();
-            case JpegDirectory.TAG_IMAGE_WIDTH:
+            case TAG_IMAGE_WIDTH:
                 return getImageWidthDescription();
             default:
@@ -68,31 +70,27 @@
     public String getImageCompressionTypeDescription()
     {
-        Integer value = _directory.getInteger(JpegDirectory.TAG_COMPRESSION_TYPE);
-        if (value==null)
-            return null;
-        // Note there is no 2 or 12
-        switch (value) {
-            case 0: return "Baseline";
-            case 1: return "Extended sequential, Huffman";
-            case 2: return "Progressive, Huffman";
-            case 3: return "Lossless, Huffman";
-            case 5: return "Differential sequential, Huffman";
-            case 6: return "Differential progressive, Huffman";
-            case 7: return "Differential lossless, Huffman";
-            case 8: return "Reserved for JPEG extensions";
-            case 9: return "Extended sequential, arithmetic";
-            case 10: return "Progressive, arithmetic";
-            case 11: return "Lossless, arithmetic";
-            case 13: return "Differential sequential, arithmetic";
-            case 14: return "Differential progressive, arithmetic";
-            case 15: return "Differential lossless, arithmetic";
-            default:
-                return "Unknown type: "+ value;
-        }
+        return getIndexedDescription(TAG_COMPRESSION_TYPE,
+            "Baseline",
+            "Extended sequential, Huffman",
+            "Progressive, Huffman",
+            "Lossless, Huffman",
+            null, // no 4
+            "Differential sequential, Huffman",
+            "Differential progressive, Huffman",
+            "Differential lossless, Huffman",
+            "Reserved for JPEG extensions",
+            "Extended sequential, arithmetic",
+            "Progressive, arithmetic",
+            "Lossless, arithmetic",
+            null, // no 12
+            "Differential sequential, arithmetic",
+            "Differential progressive, arithmetic",
+            "Differential lossless, arithmetic");
     }
+
     @Nullable
     public String getImageWidthDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_WIDTH);
+        final String value = _directory.getString(TAG_IMAGE_WIDTH);
         if (value==null)
             return null;
@@ -103,5 +101,5 @@
     public String getImageHeightDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_HEIGHT);
+        final String value = _directory.getString(TAG_IMAGE_HEIGHT);
         if (value==null)
             return null;
@@ -112,5 +110,5 @@
     public String getDataPrecisionDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_DATA_PRECISION);
+        final String value = _directory.getString(TAG_DATA_PRECISION);
         if (value==null)
             return null;
Index: trunk/src/com/drew/metadata/jpeg/JpegDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/package.html
===================================================================
--- trunk/src/com/drew/metadata/jpeg/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/jpeg/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/package.html
===================================================================
--- trunk/src/com/drew/metadata/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 10861)
+++ trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 10862)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2016 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -63,5 +63,7 @@
         _directoryStack.push(_currentDirectory);
         try {
-            _currentDirectory = directoryClass.newInstance();
+            Directory newDirectory = directoryClass.newInstance();
+            newDirectory.setParent(_currentDirectory);
+            _currentDirectory = newDirectory;
         } catch (InstantiationException e) {
             throw new RuntimeException(e);
Index: trunk/src/com/drew/metadata/tiff/package.html
===================================================================
--- trunk/src/com/drew/metadata/tiff/package.html	(revision 10861)
+++ trunk/src/com/drew/metadata/tiff/package.html	(revision 10862)
@@ -1,4 +1,4 @@
 <!--
-  ~ Copyright 2002-2015 Drew Noakes
+  ~ Copyright 2002-2016 Drew Noakes
   ~
   ~    Licensed under the Apache License, Version 2.0 (the "License");
