Index: /trunk/src/com/drew/imaging/ImageProcessingException.java
===================================================================
--- /trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -41,4 +41,6 @@
 //import com.drew.metadata.jfxx.JfxxReader;
 import com.drew.metadata.jpeg.JpegCommentReader;
+import com.drew.metadata.jpeg.JpegDhtReader;
+import com.drew.metadata.jpeg.JpegDnlReader;
 import com.drew.metadata.jpeg.JpegReader;
 //import com.drew.metadata.photoshop.DuckyReader;
@@ -63,6 +65,8 @@
             //new PhotoshopReader(),
             //new DuckyReader(),
-            new IptcReader()//,
-            //new AdobeJpegReader()
+            new IptcReader(),
+            //new AdobeJpegReader(),
+            new JpegDhtReader(),
+            new JpegDnlReader()
     );
 
Index: /trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -52,4 +52,5 @@
      * @param segmentBytes the byte array holding data for the segment being added
      */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     public void addSegment(byte segmentType, @NotNull byte[] segmentBytes)
     {
@@ -206,4 +207,5 @@
      * @param occurrence  the zero-based index of the segment occurrence to remove.
      */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
     public void removeSegmentOccurrence(@NotNull JpegSegmentType segmentType, int occurrence)
     {
@@ -218,4 +220,5 @@
      * @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/JpegSegmentMetadataReader.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java	(revision 13061)
@@ -13,5 +13,5 @@
      */
     @NotNull
-    public Iterable<JpegSegmentType> getSegmentTypes();
+    Iterable<JpegSegmentType> getSegmentTypes();
 
     /**
@@ -23,4 +23,4 @@
      * @param segmentType The {@link JpegSegmentType} being read.
      */
-    public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType);
+    void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType);
 }
Index: /trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: /trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -93,17 +93,32 @@
     DQT((byte)0xDB, false),
 
+    /** Define Number of Lines segment identifier. */
+    DNL((byte)0xDC, false),
+
+    /** Define Restart Interval segment identifier. */
+    DRI((byte)0xDD, false),
+
+    /** Define Hierarchical Progression segment identifier. */
+    DHP((byte)0xDE, false),
+
+    /** EXPand reference component(s) segment identifier. */
+    EXP((byte)0xDF, false),
+
     /** Define Huffman Table segment identifier. */
     DHT((byte)0xC4, false),
 
-    /** Start-of-Frame (0) segment identifier. */
+    /** Define Arithmetic Coding conditioning segment identifier. */
+    DAC((byte)0xCC, false),
+
+    /** Start-of-Frame (0) segment identifier for Baseline DCT. */
     SOF0((byte)0xC0, true),
 
-    /** Start-of-Frame (1) segment identifier. */
+    /** Start-of-Frame (1) segment identifier for Extended sequential DCT. */
     SOF1((byte)0xC1, true),
 
-    /** Start-of-Frame (2) segment identifier. */
+    /** Start-of-Frame (2) segment identifier for Progressive DCT. */
     SOF2((byte)0xC2, true),
 
-    /** Start-of-Frame (3) segment identifier. */
+    /** Start-of-Frame (3) segment identifier for Lossless (sequential). */
     SOF3((byte)0xC3, true),
 
@@ -111,23 +126,23 @@
 //    SOF4((byte)0xC4, true),
 
-    /** Start-of-Frame (5) segment identifier. */
+    /** Start-of-Frame (5) segment identifier for Differential sequential DCT. */
     SOF5((byte)0xC5, true),
 
-    /** Start-of-Frame (6) segment identifier. */
+    /** Start-of-Frame (6) segment identifier for Differential progressive DCT. */
     SOF6((byte)0xC6, true),
 
-    /** Start-of-Frame (7) segment identifier. */
+    /** Start-of-Frame (7) segment identifier for Differential lossless (sequential). */
     SOF7((byte)0xC7, true),
 
-    /** Start-of-Frame (8) segment identifier. */
-    SOF8((byte)0xC8, true),
+    /** Reserved for JPEG extensions. */
+    JPG((byte)0xC8, true),
 
-    /** Start-of-Frame (9) segment identifier. */
+    /** Start-of-Frame (9) segment identifier for Extended sequential DCT. */
     SOF9((byte)0xC9, true),
 
-    /** Start-of-Frame (10) segment identifier. */
+    /** Start-of-Frame (10) segment identifier for Progressive DCT. */
     SOF10((byte)0xCA, true),
 
-    /** Start-of-Frame (11) segment identifier. */
+    /** Start-of-Frame (11) segment identifier for Lossless (sequential). */
     SOF11((byte)0xCB, true),
 
@@ -135,14 +150,14 @@
 //    SOF12((byte)0xCC, true),
 
-    /** Start-of-Frame (13) segment identifier. */
+    /** Start-of-Frame (13) segment identifier for Differential sequential DCT. */
     SOF13((byte)0xCD, true),
 
-    /** Start-of-Frame (14) segment identifier. */
+    /** Start-of-Frame (14) segment identifier for Differential progressive DCT. */
     SOF14((byte)0xCE, true),
 
-    /** Start-of-Frame (15) segment identifier. */
+    /** Start-of-Frame (15) segment identifier for Differential lossless (sequential). */
     SOF15((byte)0xCF, true),
 
-    /** JPEG comment segment identifier. */
+    /** JPEG comment segment identifier for comments. */
     COM((byte)0xFE, true);
 
Index: /trunk/src/com/drew/imaging/jpeg/package-info.java
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/imaging/jpeg/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with JPEG files.
+ */
+package com.drew.imaging.jpeg;
Index: unk/src/com/drew/imaging/jpeg/package.html
===================================================================
--- /trunk/src/com/drew/imaging/jpeg/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with JPEG files.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/imaging/package-info.java
===================================================================
--- /trunk/src/com/drew/imaging/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/imaging/package-info.java	(revision 13061)
@@ -0,0 +1,5 @@
+/**
+ * Contains classes for working with image file formats and photographic conversions.
+ * <!-- Put @see and @since tags down here. -->
+ */
+package com.drew.imaging;
Index: unk/src/com/drew/imaging/package.html
===================================================================
--- /trunk/src/com/drew/imaging/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with image file formats and photographic conversions.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/imaging/tiff/TiffDataFormat.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/imaging/tiff/TiffHandler.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,4 +25,5 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 import java.io.IOException;
@@ -52,6 +53,4 @@
     void endingIFD();
 
-    void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset);
-
     @Nullable
     Long tryCustomProcessFormat(int tagId, int formatCode, long componentCount);
@@ -68,5 +67,5 @@
 
     void setByteArray(int tagId, @NotNull byte[] bytes);
-    void setString(int tagId, @NotNull String string);
+    void setString(int tagId, @NotNull StringValue string);
     void setRational(int tagId, @NotNull Rational rational);
     void setRationalArray(int tagId, @NotNull Rational[] array);
Index: /trunk/src/com/drew/imaging/tiff/TiffProcessingException.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 13060)
+++ /trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/imaging/tiff/TiffReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -77,6 +77,4 @@
         Set<Integer> processedIfdOffsets = new HashSet<Integer>();
         processIfd(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset);
-
-        handler.completed(reader, tiffHeaderOffset);
     }
 
@@ -265,5 +263,5 @@
                 break;
             case TiffDataFormat.CODE_STRING:
-                handler.setString(tagId, reader.getNullTerminatedString(tagValueOffset, componentCount));
+                handler.setString(tagId, reader.getNullTerminatedStringValue(tagValueOffset, componentCount, null));
                 break;
             case TiffDataFormat.CODE_RATIONAL_S:
Index: /trunk/src/com/drew/imaging/tiff/package-info.java
===================================================================
--- /trunk/src/com/drew/imaging/tiff/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/imaging/tiff/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with TIFF format files.
+ */
+package com.drew.imaging.tiff;
Index: unk/src/com/drew/imaging/tiff/package.html
===================================================================
--- /trunk/src/com/drew/imaging/tiff/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with TIFF format files.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- /trunk/src/com/drew/lang/BufferBoundsException.java	(revision 13060)
+++ /trunk/src/com/drew/lang/BufferBoundsException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/lang/ByteArrayReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,7 @@
 package com.drew.lang;
 
+import com.drew.lang.annotations.NotNull;
+
 import java.io.IOException;
-
-import com.drew.lang.annotations.NotNull;
 
 /**
@@ -39,11 +39,30 @@
     @NotNull
     private final byte[] _buffer;
+    private final int _baseOffset;
 
+    @SuppressWarnings({ "ConstantConditions" })
+    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
     public ByteArrayReader(@NotNull byte[] buffer)
+    {
+        this(buffer, 0);
+    }
+
+    @SuppressWarnings({ "ConstantConditions" })
+    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
+    public ByteArrayReader(@NotNull byte[] buffer, int baseOffset)
     {
         if (buffer == null)
             throw new NullPointerException();
+        if (baseOffset < 0)
+            throw new IllegalArgumentException("Must be zero or greater");
 
         _buffer = buffer;
+        _baseOffset = baseOffset;
+    }
+
+    @Override
+    public int toUnshiftedOffset(int localOffset)
+    {
+        return localOffset + _baseOffset;
     }
 
@@ -51,11 +70,12 @@
     public long getLength()
     {
-        return _buffer.length;
+        return _buffer.length - _baseOffset;
     }
 
     @Override
-    protected byte getByte(int index) throws IOException
+    public byte getByte(int index) throws IOException
     {
-        return _buffer[index];
+        validateIndex(index, 1);
+        return _buffer[index + _baseOffset];
     }
 
@@ -64,5 +84,5 @@
     {
         if (!isValidIndex(index, bytesRequested))
-            throw new BufferBoundsException(index, bytesRequested, _buffer.length);
+            throw new BufferBoundsException(toUnshiftedOffset(index), bytesRequested, _buffer.length);
     }
 
@@ -72,5 +92,5 @@
         return bytesRequested >= 0
             && index >= 0
-            && (long)index + (long)bytesRequested - 1L < _buffer.length;
+            && (long)index + (long)bytesRequested - 1L < getLength();
     }
 
@@ -82,5 +102,5 @@
 
         byte[] bytes = new byte[count];
-        System.arraycopy(_buffer, index, bytes, 0, count);
+        System.arraycopy(_buffer, index + _baseOffset, bytes, 0, count);
         return bytes;
     }
Index: /trunk/src/com/drew/lang/Charsets.java
===================================================================
--- /trunk/src/com/drew/lang/Charsets.java	(revision 13061)
+++ /trunk/src/com/drew/lang/Charsets.java	(revision 13061)
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2017 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.nio.charset.Charset;
+
+/**
+ * Holds a set of commonly used character encodings.
+ *
+ * Newer JDKs include java.nio.charset.StandardCharsets, but we cannot use that in this library.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class Charsets
+{
+    public static final Charset UTF_8 = Charset.forName("UTF-8");
+    public static final Charset UTF_16 = Charset.forName("UTF-16");
+    public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+    public static final Charset ASCII = Charset.forName("US-ASCII");
+    public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
+    public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
+}
Index: /trunk/src/com/drew/lang/CompoundException.java
===================================================================
--- /trunk/src/com/drew/lang/CompoundException.java	(revision 13060)
+++ /trunk/src/com/drew/lang/CompoundException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: /trunk/src/com/drew/lang/GeoLocation.java
===================================================================
--- /trunk/src/com/drew/lang/GeoLocation.java	(revision 13060)
+++ /trunk/src/com/drew/lang/GeoLocation.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: /trunk/src/com/drew/lang/RandomAccessReader.java
===================================================================
--- /trunk/src/com/drew/lang/RandomAccessReader.java	(revision 13060)
+++ /trunk/src/com/drew/lang/RandomAccessReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,8 +22,11 @@
 package com.drew.lang;
 
-import com.drew.lang.annotations.NotNull;
-
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 /**
@@ -36,5 +39,4 @@
  * <ul>
  *     <li>{@link ByteArrayReader}</li>
- *     <li>{@link RandomAccessStreamReader}</li>
  * </ul>
  *
@@ -45,4 +47,6 @@
     private boolean _isMotorolaByteOrder = true;
 
+    public abstract int toUnshiftedOffset(int localOffset);
+
     /**
      * Gets the byte value at the specified byte <code>index</code>.
@@ -57,5 +61,5 @@
      * @throws IOException if the byte is unable to be read
      */
-    protected abstract byte getByte(int index) throws IOException;
+    public abstract byte getByte(int index) throws IOException;
 
     /**
@@ -89,9 +93,9 @@
      * Returns the length of the data source in bytes.
      * <p>
-     * This is a simple operation for implementations (such as {@link RandomAccessFileReader} and
+     * This is a simple operation for implementations (such as
      * {@link ByteArrayReader}) that have the entire data source available.
      * <p>
-     * Users of this method must be aware that sequentially accessed implementations such as
-     * {@link RandomAccessStreamReader} will have to read and buffer the entire data source in
+     * Users of this method must be aware that sequentially accessed implementations
+     * will have to read and buffer the entire data source in
      * order to determine the length.
      *
@@ -207,10 +211,10 @@
         if (_isMotorolaByteOrder) {
             // Motorola - MSB first
-            return (short) (((short)getByte(index    ) << 8 & (short)0xFF00) |
-                            ((short)getByte(index + 1)      & (short)0xFF));
+            return (short) ((getByte(index    ) << 8 & (short)0xFF00) |
+                            (getByte(index + 1)      & (short)0xFF));
         } else {
             // Intel ordering - LSB first
-            return (short) (((short)getByte(index + 1) << 8 & (short)0xFF00) |
-                            ((short)getByte(index    )      & (short)0xFF));
+            return (short) ((getByte(index + 1) << 8 & (short)0xFF00) |
+                            (getByte(index    )      & (short)0xFF));
         }
     }
@@ -229,12 +233,12 @@
         if (_isMotorolaByteOrder) {
             // Motorola - MSB first (big endian)
-            return (((int)getByte(index    )) << 16 & 0xFF0000) |
-                   (((int)getByte(index + 1)) << 8  & 0xFF00) |
-                   (((int)getByte(index + 2))       & 0xFF);
+            return ((getByte(index    )) << 16 & 0xFF0000) |
+                   ((getByte(index + 1)) << 8  & 0xFF00) |
+                   ((getByte(index + 2))       & 0xFF);
         } else {
             // Intel ordering - LSB first (little endian)
-            return (((int)getByte(index + 2)) << 16 & 0xFF0000) |
-                   (((int)getByte(index + 1)) << 8  & 0xFF00) |
-                   (((int)getByte(index    ))       & 0xFF);
+            return ((getByte(index + 2)) << 16 & 0xFF0000) |
+                   ((getByte(index + 1)) << 8  & 0xFF00) |
+                   ((getByte(index    ))       & 0xFF);
         }
     }
@@ -256,5 +260,5 @@
                    (((long)getByte(index + 1)) << 16 & 0xFF0000L) |
                    (((long)getByte(index + 2)) << 8  & 0xFF00L) |
-                   (((long)getByte(index + 3))       & 0xFFL);
+                   ((getByte(index + 3))       & 0xFFL);
         } else {
             // Intel ordering - LSB first (little endian)
@@ -262,5 +266,5 @@
                    (((long)getByte(index + 2)) << 16 & 0xFF0000L) |
                    (((long)getByte(index + 1)) << 8  & 0xFF00L) |
-                   (((long)getByte(index    ))       & 0xFFL);
+                   ((getByte(index    ))       & 0xFFL);
         }
     }
@@ -312,5 +316,5 @@
                    ((long)getByte(index + 5) << 16 & 0xFF0000L) |
                    ((long)getByte(index + 6) << 8  & 0xFF00L) |
-                   ((long)getByte(index + 7)       & 0xFFL);
+                   (getByte(index + 7)       & 0xFFL);
         } else {
             // Intel ordering - LSB first
@@ -322,5 +326,5 @@
                    ((long)getByte(index + 2) << 16 & 0xFF0000L) |
                    ((long)getByte(index + 1) << 8  & 0xFF00L) |
-                   ((long)getByte(index    )       & 0xFFL);
+                   (getByte(index    )       & 0xFFL);
         }
     }
@@ -365,11 +369,17 @@
 
     @NotNull
-    public String getString(int index, int bytesRequested) throws IOException
-    {
-        return new String(getBytes(index, bytesRequested));
-    }
-
-    @NotNull
-    public String getString(int index, int bytesRequested, String charset) throws IOException
+    public StringValue getStringValue(int index, int bytesRequested, @Nullable Charset charset) throws IOException
+    {
+        return new StringValue(getBytes(index, bytesRequested), charset);
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested, @NotNull Charset charset) throws IOException
+    {
+        return new String(getBytes(index, bytesRequested), charset.name());
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested, @NotNull String charset) throws IOException
     {
         byte[] bytes = getBytes(index, bytesRequested);
@@ -392,16 +402,43 @@
      */
     @NotNull
-    public String getNullTerminatedString(int index, int maxLengthBytes) throws IOException
-    {
-        // NOTE currently only really suited to single-byte character strings
-
-        byte[] bytes = getBytes(index, maxLengthBytes);
+    public String getNullTerminatedString(int index, int maxLengthBytes, @NotNull Charset charset) throws IOException
+    {
+        return new String(getNullTerminatedBytes(index, maxLengthBytes), charset.name());
+    }
+
+    @NotNull
+    public StringValue getNullTerminatedStringValue(int index, int maxLengthBytes, @Nullable Charset charset) throws IOException
+    {
+        byte[] bytes = getNullTerminatedBytes(index, maxLengthBytes);
+
+        return new StringValue(bytes, charset);
+    }
+
+    /**
+     * Returns the sequence of bytes punctuated by a <code>\0</code> value.
+     *
+     * @param index The index within the buffer at which to start reading the string.
+     * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
+     * the returned array will be <code>maxLengthBytes</code> long.
+     * @return The read byte array, excluding the null terminator.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public byte[] getNullTerminatedBytes(int index, int maxLengthBytes) throws IOException
+    {
+        byte[] buffer = getBytes(index, maxLengthBytes);
 
         // Count the number of non-null bytes
         int length = 0;
-        while (length < bytes.length && bytes[length] != '\0')
+        while (length < buffer.length && buffer[length] != 0)
             length++;
 
-        return new String(bytes, 0, length);
+        if (length == maxLengthBytes)
+            return buffer;
+
+        byte[] bytes = new byte[length];
+        if (length > 0)
+            System.arraycopy(buffer, 0, bytes, 0, length);
+        return bytes;
     }
 }
Index: /trunk/src/com/drew/lang/Rational.java
===================================================================
--- /trunk/src/com/drew/lang/Rational.java	(revision 13060)
+++ /trunk/src/com/drew/lang/Rational.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -36,5 +36,6 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class Rational extends java.lang.Number implements Serializable
+@SuppressWarnings("WeakerAccess")
+public class Rational extends java.lang.Number implements Comparable<Rational>, Serializable
 {
     private static final long serialVersionUID = 510688928138848770L;
@@ -173,4 +174,10 @@
                 (_denominator != 0 && (_numerator % _denominator == 0)) ||
                 (_denominator == 0 && _numerator == 0);
+    }
+
+    /** Checks if either the numerator or denominator are zero. */
+    public boolean isZero()
+    {
+        return _numerator == 0 || _denominator == 0;
     }
 
@@ -212,14 +219,45 @@
 
     /**
-     * Decides whether a brute-force simplification calculation should be avoided
-     * by comparing the maximum number of possible calculations with some threshold.
-     *
-     * @return true if the simplification should be performed, otherwise false
-     */
-    private boolean tooComplexForSimplification()
-    {
-        double maxPossibleCalculations = (((double) (Math.min(_denominator, _numerator) - 1) / 5d) + 2);
-        final int maxSimplificationCalculations = 1000;
-        return maxPossibleCalculations > maxSimplificationCalculations;
+     * Compares two {@link Rational} instances, returning true if they are mathematically
+     * equivalent (in consistence with {@link Rational#equals(Object)} method).
+     *
+     * @param that the {@link Rational} to compare this instance to.
+     * @return the value {@code 0} if this {@link Rational} is
+     *         equal to the argument {@link Rational} mathematically; a value less
+     *         than {@code 0} if this {@link Rational} is less
+     *         than the argument {@link Rational}; and a value greater
+     *         than {@code 0} if this {@link Rational} is greater than the argument
+     *         {@link Rational}.
+     */
+    public int compareTo(@NotNull Rational that) {
+        return Double.compare(this.doubleValue(), that.doubleValue());
+    }
+
+    /**
+     * Indicates whether this instance and <code>other</code> are numerically equal,
+     * even if their representations differ.
+     *
+     * For example, 1/2 is equal to 10/20 by this method.
+     * Similarly, 1/0 is equal to 100/0 by this method.
+     * To test equal representations, use EqualsExact.
+     *
+     * @param other The rational value to compare with
+     */
+    public boolean equals(Rational other) {
+        return other.doubleValue() == doubleValue();
+    }
+
+    /**
+     * Indicates whether this instance and <code>other</code> have identical
+     * Numerator and Denominator.
+     * <p>
+     * For example, 1/2 is not equal to 10/20 by this method.
+     * Similarly, 1/0 is not equal to 100/0 by this method.
+     * To test numerically equivalence, use Equals(Rational).</p>
+     *
+     * @param other The rational value to compare with
+     */
+    public boolean equalsExact(Rational other) {
+        return getDenominator() == other.getDenominator() && getNumerator() == other.getNumerator();
     }
 
@@ -249,48 +287,37 @@
     /**
      * <p>
-     * Simplifies the {@link Rational} number.</p>
+     * Simplifies the representation of this {@link Rational} number.</p>
      * <p>
-     * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p>
+     * For example, 5/10 simplifies to 1/2 because both Numerator
+     * and Denominator share a common factor of 5.</p>
      * <p>
-     * To reduce a rational, need to see if both numerator and denominator are divisible
-     * by a common factor.  Using the prime number series in ascending order guarantees
-     * the minimum number of checks required.</p>
-     * <p>
-     * However, generating the prime number series seems to be a hefty task.  Perhaps
-     * it's simpler to check if both d &amp; n are divisible by all numbers from 2 {@literal ->}
-     * (Math.min(denominator, numerator) / 2).  In doing this, one can check for 2
-     * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5.
-     * This leaves four numbers from every ten to check.</p>
-     * <p>
-     * Therefore, the max number of pairs of modulus divisions required will be:</p>
-     * <pre><code>
-     *    4   Math.min(denominator, numerator) - 1
-     *   -- * ------------------------------------ + 2
-     *   10                    2
-     *
-     *   Math.min(denominator, numerator) - 1
-     * = ------------------------------------ + 2
-     *                  5
-     * </code></pre>
-     *
-     * @return a simplified instance, or if the Rational could not be simplified,
-     *         returns itself (unchanged)
+     * Uses the Euclidean Algorithm to find the greatest common divisor.</p>
+     *
+     * @return A simplified instance if one exists, otherwise a copy of the original value.
      */
     @NotNull
     public Rational getSimplifiedInstance()
     {
-        if (tooComplexForSimplification()) {
-            return this;
+        long gcd = GCD(_numerator, _denominator);
+
+        return new Rational(_numerator / gcd, _denominator / gcd);
+    }
+
+    private static long GCD(long a, long b)
+    {
+        if (a < 0)
+            a = -a;
+        if (b < 0)
+            b = -b;
+
+        while (a != 0 && b != 0)
+        {
+            if (a > b)
+                a %= b;
+            else
+                b %= a;
         }
-        for (int factor = 2; factor <= Math.min(_denominator, _numerator); factor++) {
-            if ((factor % 2 == 0 && factor > 2) || (factor % 5 == 0 && factor > 5)) {
-                continue;
-            }
-            if (_denominator % factor == 0 && _numerator % factor == 0) {
-                // found a common factor
-                return new Rational(_numerator / factor, _denominator / factor);
-            }
-        }
-        return this;
+
+        return a == 0 ? b : a;
     }
 }
Index: /trunk/src/com/drew/lang/SequentialByteArrayReader.java
===================================================================
--- /trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 13060)
+++ /trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +37,10 @@
     private int _index;
 
+    @Override
+    public long getPosition()
+    {
+        return _index;
+    }
+
     public SequentialByteArrayReader(@NotNull byte[] bytes)
     {
@@ -42,4 +48,5 @@
     }
 
+    @SuppressWarnings("ConstantConditions")
     public SequentialByteArrayReader(@NotNull byte[] bytes, int baseIndex)
     {
@@ -52,5 +59,5 @@
 
     @Override
-    protected byte getByte() throws IOException
+    public byte getByte() throws IOException
     {
         if (_index >= _bytes.length) {
@@ -73,4 +80,15 @@
 
         return bytes;
+    }
+
+    @Override
+    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
+    {
+        if (_index + count > _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+
+        System.arraycopy(_bytes, _index, buffer, offset, count);
+        _index += count;
     }
 
@@ -105,3 +123,8 @@
         return true;
     }
+
+    @Override
+    public int available() {
+        return _bytes.length - _index;
+    }
 }
Index: /trunk/src/com/drew/lang/SequentialReader.java
===================================================================
--- /trunk/src/com/drew/lang/SequentialReader.java	(revision 13060)
+++ /trunk/src/com/drew/lang/SequentialReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,12 +23,16 @@
 
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public abstract class SequentialReader
 {
@@ -37,4 +41,6 @@
     private boolean _isMotorolaByteOrder = true;
 
+    public abstract long getPosition() throws IOException;
+
     /**
      * Gets the next byte in the sequence.
@@ -42,5 +48,5 @@
      * @return The read byte value
      */
-    protected abstract byte getByte() throws IOException;
+    public abstract byte getByte() throws IOException;
 
     /**
@@ -52,4 +58,12 @@
     @NotNull
     public abstract byte[] getBytes(int count) throws IOException;
+
+    /**
+     * Retrieves bytes, writing them into a caller-provided buffer.
+     * @param buffer The array to write bytes to.
+     * @param offset The starting position within buffer to write to.
+     * @param count The number of bytes to be written.
+     */
+    public abstract void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException;
 
     /**
@@ -70,4 +84,22 @@
      */
     public abstract boolean trySkip(long n) throws IOException;
+
+    /**
+     * Returns an estimate of the number of bytes that can be read (or skipped
+     * over) from this {@link SequentialReader} without blocking by the next
+     * invocation of a method for this input stream. A single read or skip of
+     * this many bytes will not block, but may read or skip fewer bytes.
+     * <p>
+     * Note that while some implementations of {@link SequentialReader} like
+     * {@link SequentialByteArrayReader} will return the total remaining number
+     * of bytes in the stream, others will not. It is never correct to use the
+     * return value of this method to allocate a buffer intended to hold all
+     * data in this stream.
+     *
+     * @return an estimate of the number of bytes that can be read (or skipped
+     *         over) from this {@link SequentialReader} without blocking or
+     *         {@code 0} when it reaches the end of the input stream.
+     */
+    public abstract int available();
 
     /**
@@ -284,4 +316,17 @@
     }
 
+    @NotNull
+    public String getString(int bytesRequested, @NotNull Charset charset) throws IOException
+    {
+        byte[] bytes = getBytes(bytesRequested);
+        return new String(bytes, charset);
+    }
+
+    @NotNull
+    public StringValue getStringValue(int bytesRequested, @Nullable Charset charset) throws IOException
+    {
+        return new StringValue(getBytes(bytesRequested), charset);
+    }
+
     /**
      * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
@@ -293,16 +338,52 @@
      */
     @NotNull
-    public String getNullTerminatedString(int maxLengthBytes) throws IOException
-    {
-        // NOTE currently only really suited to single-byte character strings
-
-        byte[] bytes = new byte[maxLengthBytes];
+    public String getNullTerminatedString(int maxLengthBytes, Charset charset) throws IOException
+    {
+       return getNullTerminatedStringValue(maxLengthBytes, charset).toString();
+    }
+
+    /**
+     * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
+     *
+     * @param maxLengthBytes The maximum number of bytes to read.  If a <code>\0</code> byte is not reached within this limit,
+     *                       reading will stop and the string will be truncated to this length.
+     * @param charset The <code>Charset</code> to register with the returned <code>StringValue</code>, or <code>null</code> if the encoding
+     *                is unknown
+     * @return The read string.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public StringValue getNullTerminatedStringValue(int maxLengthBytes, Charset charset) throws IOException
+    {
+        byte[] bytes = getNullTerminatedBytes(maxLengthBytes);
+
+        return new StringValue(bytes, charset);
+    }
+
+    /**
+     * Returns the sequence of bytes punctuated by a <code>\0</code> value.
+     *
+     * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
+     * the returned array will be <code>maxLengthBytes</code> long.
+     * @return The read byte array, excluding the null terminator.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public byte[] getNullTerminatedBytes(int maxLengthBytes) throws IOException
+    {
+        byte[] buffer = new byte[maxLengthBytes];
 
         // Count the number of non-null bytes
         int length = 0;
-        while (length < bytes.length && (bytes[length] = getByte()) != '\0')
+        while (length < buffer.length && (buffer[length] = getByte()) != 0)
             length++;
 
-        return new String(bytes, 0, length);
+        if (length == maxLengthBytes)
+            return buffer;
+
+        byte[] bytes = new byte[length];
+        if (length > 0)
+            System.arraycopy(buffer, 0, bytes, 0, length);
+        return bytes;
     }
 }
Index: /trunk/src/com/drew/lang/StreamReader.java
===================================================================
--- /trunk/src/com/drew/lang/StreamReader.java	(revision 13060)
+++ /trunk/src/com/drew/lang/StreamReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +37,13 @@
     private final InputStream _stream;
 
+    private long _pos;
+
+    @Override
+    public long getPosition()
+    {
+        return _pos;
+    }
+
+    @SuppressWarnings("ConstantConditions")
     public StreamReader(@NotNull InputStream stream)
     {
@@ -43,12 +52,14 @@
 
         _stream = stream;
+        _pos = 0;
     }
 
     @Override
-    protected byte getByte() throws IOException
+    public byte getByte() throws IOException
     {
         int value = _stream.read();
         if (value == -1)
             throw new EOFException("End of data reached.");
+        _pos++;
         return (byte)value;
     }
@@ -59,8 +70,15 @@
     {
         byte[] bytes = new byte[count];
+        getBytes(bytes, 0, count);
+        return bytes;
+    }
+
+    @Override
+    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
+    {
         int totalBytesRead = 0;
-
-        while (totalBytesRead != count) {
-            final int bytesRead = _stream.read(bytes, totalBytesRead, count - totalBytesRead);
+        while (totalBytesRead != count)
+        {
+            final int bytesRead = _stream.read(buffer, offset + totalBytesRead, count - totalBytesRead);
             if (bytesRead == -1)
                 throw new EOFException("End of data reached.");
@@ -68,6 +86,5 @@
             assert(totalBytesRead <= count);
         }
-
-        return bytes;
+        _pos += totalBytesRead;
     }
 
@@ -93,4 +110,13 @@
     }
 
+    @Override
+    public int available() {
+        try {
+            return _stream.available();
+        } catch (IOException e) {
+            return 0;
+        }
+    }
+
     private long skipInternal(long n) throws IOException
     {
@@ -109,4 +135,5 @@
                 break;
         }
+        _pos += skippedTotal;
         return skippedTotal;
     }
Index: /trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- /trunk/src/com/drew/lang/StringUtil.java	(revision 13060)
+++ /trunk/src/com/drew/lang/StringUtil.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,5 +34,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class StringUtil
+public final class StringUtil
 {
     @NotNull
Index: /trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- /trunk/src/com/drew/lang/annotations/NotNull.java	(revision 13060)
+++ /trunk/src/com/drew/lang/annotations/NotNull.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/lang/annotations/Nullable.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13061)
+++ /trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 13061)
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2011 Andreas Ziermann
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    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-info.java
===================================================================
--- /trunk/src/com/drew/lang/annotations/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/lang/annotations/package-info.java	(revision 13061)
@@ -0,0 +1,5 @@
+/**
+ * Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA
+ * to provide design-time warnings about potential run-time errors.
+ */
+package com.drew.lang.annotations;
Index: unk/src/com/drew/lang/annotations/package.html
===================================================================
--- /trunk/src/com/drew/lang/annotations/package.html	(revision 13060)
+++ 	(revision )
@@ -1,34 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA
-to provide design-time warnings about potential run-time errors.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/lang/package-info.java
===================================================================
--- /trunk/src/com/drew/lang/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/lang/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes of generic utility.
+ */
+package com.drew.lang;
Index: unk/src/com/drew/lang/package.html
===================================================================
--- /trunk/src/com/drew/lang/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes of generic utility.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/Age.java
===================================================================
--- /trunk/src/com/drew/metadata/Age.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/Age.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -51,7 +51,4 @@
     public static Age fromPanasonicString(@NotNull String s)
     {
-        if (s == null)
-            throw new NullPointerException();
-
         if (s.length() != 19 || s.startsWith("9999:99:99"))
             return null;
@@ -143,5 +140,5 @@
 
     @Override
-    public boolean equals(Object o)
+    public boolean equals(@Nullable Object o)
     {
         if (this == o) return true;
Index: /trunk/src/com/drew/metadata/Directory.java
===================================================================
--- /trunk/src/com/drew/metadata/Directory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/Directory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,4 +24,5 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
+import com.drew.lang.annotations.SuppressWarnings;
 
 import java.io.UnsupportedEncodingException;
@@ -41,7 +42,8 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@java.lang.SuppressWarnings("WeakerAccess")
 public abstract class Directory
 {
-    private static final DecimalFormat _floatFormat = new DecimalFormat("0.###");
+    private static final String _floatFormatPattern = "0.###";
 
     /** Map of values hashed by type identifiers. */
@@ -260,4 +262,18 @@
 
     /**
+     * Sets a <code>StringValue</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a StringValue
+     */
+    @java.lang.SuppressWarnings({ "ConstantConditions" })
+    public void setStringValue(int tagType, @NotNull StringValue value)
+    {
+        if (value == null)
+            throw new NullPointerException("cannot set a null StringValue");
+        setObject(tagType, value);
+    }
+
+    /**
      * Sets a <code>String</code> value for the specified tag.
      *
@@ -280,4 +296,15 @@
      */
     public void setStringArray(int tagType, @NotNull String[] strings)
+    {
+        setObjectArray(tagType, strings);
+    }
+
+    /**
+     * Sets a <code>StringValue[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param strings the StringValue array to store
+     */
+    public void setStringValueArray(int tagType, @NotNull StringValue[] strings)
     {
         setObjectArray(tagType, strings);
@@ -440,10 +467,10 @@
         if (o instanceof Number) {
             return ((Number)o).intValue();
-        } else if (o instanceof String) {
+        } else if (o instanceof String || o instanceof StringValue) {
             try {
-                return Integer.parseInt((String)o);
+                return Integer.parseInt(o.toString());
             } catch (NumberFormatException nfe) {
                 // convert the char array to an int
-                String s = (String)o;
+                String s = o.toString();
                 byte[] bytes = s.getBytes();
                 long val = 0;
@@ -466,4 +493,8 @@
             if (ints.length == 1)
                 return ints[0];
+        } else if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            if (shorts.length == 1)
+                return (int)shorts[0];
         }
         return null;
@@ -472,5 +503,5 @@
     /**
      * Gets the specified tag's value as a String array, if possible.  Only supported
-     * where the tag is set as String[], String, int[], byte[] or Rational[].
+     * where the tag is set as StringValue[], String[], StringValue, String, int[], byte[] or Rational[].
      *
      * @param tagType the tag identifier
@@ -487,4 +518,13 @@
         if (o instanceof String)
             return new String[] { (String)o };
+        if (o instanceof StringValue)
+            return new String[] { o.toString() };
+        if (o instanceof StringValue[]) {
+            StringValue[] stringValues = (StringValue[])o;
+            String[] strings = new String[stringValues.length];
+            for (int i = 0; i < strings.length; i++)
+                strings[i] = stringValues[i].toString();
+            return strings;
+        }
         if (o instanceof int[]) {
             int[] ints = (int[])o;
@@ -493,5 +533,6 @@
                 strings[i] = Integer.toString(ints[i]);
             return strings;
-        } else if (o instanceof byte[]) {
+        }
+        if (o instanceof byte[]) {
             byte[] bytes = (byte[])o;
             String[] strings = new String[bytes.length];
@@ -499,5 +540,6 @@
                 strings[i] = Byte.toString(bytes[i]);
             return strings;
-        } else if (o instanceof Rational[]) {
+        }
+        if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
             String[] strings = new String[rationals.length];
@@ -506,4 +548,24 @@
             return strings;
         }
+        return null;
+    }
+
+    /**
+     * Gets the specified tag's value as a StringValue array, if possible.
+     * Only succeeds if the tag is set as StringValue[], or StringValue.
+     *
+     * @param tagType the tag identifier
+     * @return the tag's value as an array of StringValues. If the value is unset or cannot be converted, <code>null</code> is returned.
+     */
+    @Nullable
+    public StringValue[] getStringValueArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof StringValue[])
+            return (StringValue[])o;
+        if (o instanceof StringValue)
+            return new StringValue[] {(StringValue) o};
         return null;
     }
@@ -575,4 +637,6 @@
         if (o == null) {
             return null;
+        } else if (o instanceof StringValue) {
+            return ((StringValue)o).getBytes();
         } else if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
@@ -630,7 +694,7 @@
         if (o == null)
             return null;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Double.parseDouble((String)o);
+                return Double.parseDouble(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
@@ -662,7 +726,7 @@
         if (o == null)
             return null;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Float.parseFloat((String)o);
+                return Float.parseFloat(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
@@ -678,5 +742,5 @@
     {
         Long value = getLongObject(tagType);
-        if (value!=null)
+        if (value != null)
             return value;
         Object o = getObject(tagType);
@@ -693,7 +757,7 @@
         if (o == null)
             return null;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Long.parseLong((String)o);
+                return Long.parseLong(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
@@ -709,5 +773,5 @@
     {
         Boolean value = getBooleanObject(tagType);
-        if (value!=null)
+        if (value != null)
             return value;
         Object o = getObject(tagType);
@@ -719,4 +783,5 @@
     /** 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)
     {
@@ -726,7 +791,7 @@
         if (o instanceof Boolean)
             return (Boolean)o;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Boolean.getBoolean((String)o);
+                return Boolean.getBoolean(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
@@ -788,5 +853,5 @@
         java.util.Date date = null;
 
-        if (o instanceof String) {
+        if ((o instanceof String) || (o instanceof StringValue)) {
             // 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
@@ -802,6 +867,8 @@
                     "yyyy-MM-dd",
                     "yyyy-MM",
+                    "yyyyMMdd", // as used in IPTC data
                     "yyyy" };
-            String dateString = (String)o;
+
+            String dateString = o.toString();
 
             // if the date string has subsecond information, it supersedes the subsecond parameter
@@ -945,5 +1012,5 @@
                     if (i != 0)
                         string.append(' ');
-                    string.append(_floatFormat.format(Array.getFloat(o, i)));
+                    string.append(new DecimalFormat(_floatFormatPattern).format(Array.getFloat(o, i)));
                 }
             } else if (componentType.getName().equals("double")) {
@@ -951,5 +1018,5 @@
                     if (i != 0)
                         string.append(' ');
-                    string.append(_floatFormat.format(Array.getDouble(o, i)));
+                    string.append(new DecimalFormat(_floatFormatPattern).format(Array.getDouble(o, i)));
                 }
             } else if (componentType.getName().equals("byte")) {
@@ -967,8 +1034,8 @@
 
         if (o instanceof Double)
-            return _floatFormat.format(((Double)o).doubleValue());
+            return new DecimalFormat(_floatFormatPattern).format(((Double)o).doubleValue());
 
         if (o instanceof Float)
-            return _floatFormat.format(((Float)o).floatValue());
+            return new DecimalFormat(_floatFormatPattern).format(((Float)o).floatValue());
 
         // Note that several cameras leave trailing spaces (Olympus, Nikon) but this library is intended to show
@@ -990,4 +1057,13 @@
             return null;
         }
+    }
+
+    @Nullable
+    public StringValue getStringValue(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o instanceof StringValue)
+            return (StringValue)o;
+        return null;
     }
 
Index: /trunk/src/com/drew/metadata/ErrorDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/ErrorDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/ErrorDirectory.java	(revision 13061)
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2017 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;
+
+import com.drew.lang.annotations.NotNull;
+import java.util.*;
+
+/**
+ * A directory to use for the reporting of errors. No values may be added to this directory, only warnings and errors.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+
+public final class ErrorDirectory extends Directory
+{
+
+    public ErrorDirectory()
+    {}
+
+    public ErrorDirectory(String error)
+    {
+        super.addError(error);
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Error";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return new HashMap<Integer, String>();
+    }
+
+    @Override
+    @NotNull
+    public String getTagName(int tagType)
+    {
+        return "";
+    }
+
+    @Override
+    public boolean hasTagName(int tagType)
+    {
+        return false;
+    }
+
+    @Override
+    public void setObject(int tagType, @NotNull Object value)
+    {
+        throw new UnsupportedOperationException(String.format("Cannot add value to %s.", ErrorDirectory.class.getName()));
+    }
+}
Index: /trunk/src/com/drew/metadata/Face.java
===================================================================
--- /trunk/src/com/drew/metadata/Face.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/Face.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 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 13060)
+++ /trunk/src/com/drew/metadata/Metadata.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -53,5 +53,5 @@
     }
 
-    @Nullable
+    @NotNull
     @SuppressWarnings("unchecked")
     public <T extends Directory> Collection<T> getDirectoriesOfType(Class<T> type)
Index: /trunk/src/com/drew/metadata/MetadataException.java
===================================================================
--- /trunk/src/com/drew/metadata/MetadataException.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/MetadataException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: /trunk/src/com/drew/metadata/StringValue.java
===================================================================
--- /trunk/src/com/drew/metadata/StringValue.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/StringValue.java	(revision 13061)
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2017 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;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class StringValue
+{
+    @NotNull
+    private final byte[] _bytes;
+
+    @Nullable
+    private final Charset _charset;
+
+    public StringValue(@NotNull byte[] bytes, @Nullable Charset charset)
+    {
+        _bytes = bytes;
+        _charset = charset;
+    }
+
+    @NotNull
+    public byte[] getBytes()
+    {
+        return _bytes;
+    }
+
+    @Nullable
+    public Charset getCharset()
+    {
+        return _charset;
+    }
+
+    @Override
+    public String toString()
+    {
+        return toString(_charset);
+    }
+
+    public String toString(@Nullable Charset charset)
+    {
+        if (charset != null) {
+            try {
+                return new String(_bytes, charset.name());
+            } catch (UnsupportedEncodingException ex) {
+                // fall through
+            }
+        }
+
+        return new String(_bytes);
+    }
+}
Index: /trunk/src/com/drew/metadata/Tag.java
===================================================================
--- /trunk/src/com/drew/metadata/Tag.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/Tag.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,4 +30,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("unused")
 public class Tag
 {
@@ -84,5 +85,4 @@
      * @return whether this tag has a name
      */
-    @NotNull
     public boolean hasTagName()
     {
Index: /trunk/src/com/drew/metadata/TagDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/TagDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/TagDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,4 +29,5 @@
 import java.lang.reflect.Array;
 import java.math.RoundingMode;
+import java.nio.charset.Charset;
 import java.text.DecimalFormat;
 import java.text.SimpleDateFormat;
@@ -74,5 +75,5 @@
             final int length = Array.getLength(object);
             if (length > 16) {
-                return String.format("[%d %s]", length, length == 1 ? "value" : "values");
+                return String.format("[%d values]", length);
             }
         }
@@ -269,5 +270,5 @@
 
     @Nullable
-    protected String getAsciiStringFromBytes(int tag)
+    protected String getStringFromBytes(int tag, Charset cs)
     {
         byte[] values = _directory.getByteArray(tag);
@@ -277,5 +278,5 @@
 
         try {
-            return new String(values, "ASCII").trim();
+            return new String(values, cs.name()).trim();
         } catch (UnsupportedEncodingException e) {
             return null;
@@ -321,5 +322,5 @@
         Rational[] values = _directory.getRationalArray(tag);
 
-        if (values == null || values.length != 4 || (values[0].doubleValue() == 0 && values[2].doubleValue() == 0))
+        if (values == null || values.length != 4 || (values[0].isZero() && values[2].isZero()))
             return null;
 
@@ -331,5 +332,5 @@
             sb.append(values[0].toSimpleString(true)).append('-').append(values[1].toSimpleString(true)).append("mm");
 
-        if (values[2].doubleValue() != 0) {
+        if (!values[2].isZero()) {
             sb.append(' ');
 
@@ -345,3 +346,121 @@
         return sb.toString();
     }
+
+    @Nullable
+    protected String getOrientationDescription(int tag)
+    {
+        return getIndexedDescription(tag, 1,
+            "Top, left side (Horizontal / normal)",
+            "Top, right side (Mirror horizontal)",
+            "Bottom, right side (Rotate 180)",
+            "Bottom, left side (Mirror vertical)",
+            "Left side, top (Mirror horizontal and rotate 270 CW)",
+            "Right side, top (Rotate 90 CW)",
+            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
+            "Left side, bottom (Rotate 270 CW)");
+    }
+
+    @Nullable
+    protected String getShutterSpeedDescription(int tag)
+    {
+        // I believe this method to now be stable, but am leaving some alternative snippets of
+        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
+
+//        float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+//        int apexPower = (int)Math.pow(2.0, apexValue);
+//        return "1/" + apexPower + " sec";
+        // TODO test this method
+        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
+        // description (spotted bug using a Canon EOS 300D)
+        // thanks also to Gli Blr for spotting this bug
+        Float apexValue = _directory.getFloatObject(tag);
+        if (apexValue == null)
+            return null;
+        if (apexValue <= 1) {
+            float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
+            long apexPower10 = Math.round((double)apexPower * 10.0);
+            float fApexPower = (float)apexPower10 / 10.0f;
+            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))));
+            return "1/" + apexPower + " sec";
+        }
+
+/*
+        // This alternative implementation offered by Bill Richards
+        // TODO determine which is the correct / more-correct implementation
+        double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+        double apexPower = Math.pow(2.0, apexValue);
+
+        StringBuffer sb = new StringBuffer();
+        if (apexPower > 1)
+            apexPower = Math.floor(apexPower);
+
+        if (apexPower < 1) {
+            sb.append((int)Math.round(1/apexPower));
+        } else {
+            sb.append("1/");
+            sb.append((int)apexPower);
+        }
+        sb.append(" sec");
+        return sb.toString();
+*/
+    }
+
+    // EXIF LightSource
+    @Nullable
+    protected String getLightSourceDescription(short wbtype)
+    {
+        switch (wbtype)
+        {
+            case 0:
+                return "Unknown";
+            case 1:
+                return "Daylight";
+            case 2:
+                return "Fluorescent";
+            case 3:
+                return "Tungsten (Incandescent)";
+            case 4:
+                return "Flash";
+            case 9:
+                return "Fine Weather";
+            case 10:
+                return "Cloudy";
+            case 11:
+                return "Shade";
+            case 12:
+                return "Daylight Fluorescent";    // (D 5700 - 7100K)
+            case 13:
+                return "Day White Fluorescent";   // (N 4600 - 5500K)
+            case 14:
+                return "Cool White Fluorescent";  // (W 3800 - 4500K)
+            case 15:
+                return "White Fluorescent";       // (WW 3250 - 3800K)
+            case 16:
+                return "Warm White Fluorescent";  // (L 2600 - 3250K)
+            case 17:
+                return "Standard Light A";
+            case 18:
+                return "Standard Light B";
+            case 19:
+                return "Standard Light C";
+            case 20:
+                return "D55";
+            case 21:
+                return "D65";
+            case 22:
+                return "D75";
+            case 23:
+                return "D50";
+            case 24:
+                return "ISO Studio Tungsten";
+            case 255:
+                return "Other";
+        }
+
+        return getDescription(wbtype);
+    }
 }
Index: /trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,9 +26,10 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
+import com.drew.lang.ByteArrayReader;
 import com.drew.metadata.Directory;
 import com.drew.metadata.TagDescriptor;
 
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
-import java.math.RoundingMode;
 import java.text.DecimalFormat;
 import java.util.HashMap;
@@ -42,4 +43,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public abstract class ExifDescriptorBase<T extends Directory> extends TagDescriptor<T>
 {
@@ -49,7 +51,4 @@
      */
     private final boolean _allowDecimalRepresentationOfRationals = true;
-
-    @NotNull
-    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
 
     // Note for the potential addition of brightness presentation in eV:
@@ -123,4 +122,6 @@
             case TAG_FILL_ORDER:
                 return getFillOrderDescription();
+            case TAG_CFA_PATTERN_2:
+                return getCfaPattern2Description();
             case TAG_EXPOSURE_TIME:
                 return getExposureTimeDescription();
@@ -167,4 +168,6 @@
             case TAG_SCENE_TYPE:
                 return getSceneTypeDescription();
+            case TAG_CFA_PATTERN:
+                return getCfaPatternDescription();
             case TAG_COMPONENTS_CONFIGURATION:
                 return getComponentConfigurationDescription();
@@ -234,7 +237,24 @@
     public String getReferenceBlackWhiteDescription()
     {
+        // For some reason, sometimes this is read as a long[] and
+        // getIntArray isn't able to deal with it
         int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
         if (ints==null || ints.length < 6)
-            return null;
+        {
+            Object o = _directory.getObject(TAG_REFERENCE_BLACK_WHITE);
+            if (o != null && (o instanceof long[]))
+            {
+                long[] longs = (long[])o;
+                if (longs.length < 6)
+                    return null;
+
+                ints = new int[longs.length];
+                for (int i = 0; i < longs.length; i++)
+                    ints[i] = (int)longs[i];
+            }
+            else
+                return null;
+        }
+
         int blackR = ints[0];
         int whiteR = ints[1];
@@ -279,13 +299,5 @@
     public String getOrientationDescription()
     {
-        return getIndexedDescription(TAG_ORIENTATION, 1,
-            "Top, left side (Horizontal / normal)",
-            "Top, right side (Mirror horizontal)",
-            "Bottom, right side (Rotate 180)",
-            "Bottom, left side (Mirror vertical)",
-            "Left side, top (Mirror horizontal and rotate 270 CW)",
-            "Right side, top (Rotate 90 CW)",
-            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
-            "Left side, bottom (Rotate 270 CW)");
+        return super.getOrientationDescription(TAG_ORIENTATION);
     }
 
@@ -443,7 +455,8 @@
     public String getNewSubfileTypeDescription()
     {
-        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1,
+        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 0,
             "Full-resolution image",
             "Reduced-resolution image",
+            "Single page of multi-page image",
             "Single page of multi-page reduced-resolution image",
             "Transparency mask",
@@ -587,5 +600,5 @@
             : value.getNumerator() == 0
                 ? "Digital zoom not used"
-                : SimpleDecimalFormatter.format(value.doubleValue());
+                : new DecimalFormat("0.#").format(value.doubleValue());
     }
 
@@ -689,4 +702,160 @@
             "Directly photographed image"
         );
+    }
+
+    /// <summary>
+    /// String description of CFA Pattern
+    /// </summary>
+    /// <remarks>
+    /// Converted from Exiftool version 10.33 created by Phil Harvey
+    /// http://www.sno.phy.queensu.ca/~phil/exiftool/
+    /// lib\Image\ExifTool\Exif.pm
+    ///
+    /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used.
+    /// It does not apply to all sensing methods.
+    /// </remarks>
+    @Nullable
+    public String getCfaPatternDescription()
+    {
+        return formatCFAPattern(decodeCfaPattern(TAG_CFA_PATTERN));
+    }
+
+    /// <summary>
+    /// String description of CFA Pattern
+    /// </summary>
+    /// <remarks>
+    /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used.
+    /// It does not apply to all sensing methods.
+    ///
+    /// ExifDirectoryBase.TAG_CFA_PATTERN_2 holds only the pixel pattern. ExifDirectoryBase.TAG_CFA_REPEAT_PATTERN_DIM is expected to exist and pass
+    /// some conditional tests.
+    /// </remarks>
+    @Nullable
+    public String getCfaPattern2Description()
+    {
+        byte[] values = _directory.getByteArray(TAG_CFA_PATTERN_2);
+        if (values == null)
+            return null;
+
+        int[] repeatPattern = _directory.getIntArray(TAG_CFA_REPEAT_PATTERN_DIM);
+        if (repeatPattern == null)
+            return String.format("Repeat Pattern not found for CFAPattern (%s)", super.getDescription(TAG_CFA_PATTERN_2));
+
+        if (repeatPattern.length == 2 && values.length == (repeatPattern[0] * repeatPattern[1]))
+        {
+            int[] intpattern = new int[2 + values.length];
+            intpattern[0] = repeatPattern[0];
+            intpattern[1] = repeatPattern[1];
+
+            for (int i = 0; i < values.length; i++)
+                intpattern[i + 2] = values[i] & 0xFF;   // convert the values[i] byte to unsigned
+
+            return formatCFAPattern(intpattern);
+        }
+
+        return String.format("Unknown Pattern (%s)", super.getDescription(TAG_CFA_PATTERN_2));
+    }
+
+    @Nullable
+    private static String formatCFAPattern(@Nullable int[] pattern)
+    {
+        if (pattern == null)
+            return null;
+        if (pattern.length < 2)
+            return "<truncated data>";
+        if (pattern[0] == 0 && pattern[1] == 0)
+            return "<zero pattern size>";
+
+        int end = 2 + pattern[0] * pattern[1];
+        if (end > pattern.length)
+            return "<invalid pattern size>";
+
+        String[] cfaColors = { "Red", "Green", "Blue", "Cyan", "Magenta", "Yellow", "White" };
+
+        StringBuilder ret = new StringBuilder();
+        ret.append("[");
+        for (int pos = 2; pos < end; pos++)
+        {
+            if (pattern[pos] <= cfaColors.length - 1)
+                ret.append(cfaColors[pattern[pos]]);
+            else
+                ret.append("Unknown");      // indicated pattern position is outside the array bounds
+
+            if ((pos - 2) % pattern[1] == 0)
+                ret.append(",");
+            else if(pos != end - 1)
+                ret.append("][");
+        }
+        ret.append("]");
+
+        return ret.toString();
+    }
+
+    /// <summary>
+    /// Decode raw CFAPattern value
+    /// </summary>
+    /// <remarks>
+    /// Converted from Exiftool version 10.33 created by Phil Harvey
+    /// http://www.sno.phy.queensu.ca/~phil/exiftool/
+    /// lib\Image\ExifTool\Exif.pm
+    ///
+    /// The value consists of:
+    /// - Two short, being the grid width and height of the repeated pattern.
+    /// - Next, for every pixel in that pattern, an identification code.
+    /// </remarks>
+    @Nullable
+    private int[] decodeCfaPattern(int tagType)
+    {
+        int[] ret;
+
+        byte[] values = _directory.getByteArray(tagType);
+        if (values == null)
+            return null;
+
+        if (values.length < 4)
+        {
+            ret = new int[values.length];
+            for (int i = 0; i < values.length; i++)
+                ret[i] = values[i];
+            return ret;
+        }
+
+        ret = new int[values.length - 2];
+
+        try {
+            ByteArrayReader reader = new ByteArrayReader(values);
+
+            // first two values should be read as 16-bits (2 bytes)
+            short item0 = reader.getInt16(0);
+            short item1 = reader.getInt16(2);
+
+            Boolean copyArray = false;
+            int end = 2 + item0 * item1;
+            if (end > values.length) // sanity check in case of byte order problems; calculated 'end' should be <= length of the values
+            {
+                // try swapping byte order (I have seen this order different than in EXIF)
+                reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder());
+                item0 = reader.getInt16(0);
+                item1 = reader.getInt16(2);
+
+                if (values.length >= (2 + item0 * item1))
+                    copyArray = true;
+            }
+            else
+                copyArray = true;
+
+            if(copyArray)
+            {
+                ret[0] = item0;
+                ret[1] = item1;
+
+                for (int i = 4; i < values.length; i++)
+                    ret[i - 2] = reader.getInt8(i);
+            }
+        } catch (IOException ex) {
+            _directory.addError("IO exception processing data: " + ex.getMessage());
+        }
+
+        return ret;
     }
 
@@ -1002,48 +1171,5 @@
     public String getShutterSpeedDescription()
     {
-        // I believe this method to now be stable, but am leaving some alternative snippets of
-        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
-
-//        float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
-//        int apexPower = (int)Math.pow(2.0, apexValue);
-//        return "1/" + apexPower + " sec";
-        // TODO test this method
-        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
-        // description (spotted bug using a Canon EOS 300D)
-        // thanks also to Gli Blr for spotting this bug
-        Float apexValue = _directory.getFloatObject(TAG_SHUTTER_SPEED);
-        if (apexValue == null)
-            return null;
-        if (apexValue <= 1) {
-            float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
-            long apexPower10 = Math.round((double)apexPower * 10.0);
-            float fApexPower = (float)apexPower10 / 10.0f;
-            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))));
-            return "1/" + apexPower + " sec";
-        }
-
-/*
-        // This alternative implementation offered by Bill Richards
-        // TODO determine which is the correct / more-correct implementation
-        double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
-        double apexPower = Math.pow(2.0, apexValue);
-
-        StringBuffer sb = new StringBuffer();
-        if (apexPower > 1)
-            apexPower = Math.floor(apexPower);
-
-        if (apexPower < 1) {
-            sb.append((int)Math.round(1/apexPower));
-        } else {
-            sb.append("1/");
-            sb.append((int)apexPower);
-        }
-        sb.append(" sec");
-        return sb.toString();
-*/
+        return super.getShutterSpeedDescription(TAG_SHUTTER_SPEED);
     }
 
Index: /trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public abstract class ExifDirectoryBase extends Directory
 {
@@ -126,4 +127,6 @@
 
     public static final int TAG_RESOLUTION_UNIT                   = 0x0128;
+    public static final int TAG_PAGE_NUMBER                       = 0x0129;
+
     public static final int TAG_TRANSFER_FUNCTION                 = 0x012D;
     public static final int TAG_SOFTWARE                          = 0x0131;
@@ -149,4 +152,13 @@
     public static final int TAG_JPEG_TABLES                       = 0x015B;
     public static final int TAG_JPEG_PROC                         = 0x0200;
+
+    // 0x0201 can have all kinds of descriptions for thumbnail starting index
+    // 0x0202 can have all kinds of descriptions for thumbnail length
+    public static final int TAG_JPEG_RESTART_INTERVAL = 0x0203;
+    public static final int TAG_JPEG_LOSSLESS_PREDICTORS = 0x0205;
+    public static final int TAG_JPEG_POINT_TRANSFORMS = 0x0206;
+    public static final int TAG_JPEG_Q_TABLES = 0x0207;
+    public static final int TAG_JPEG_DC_TABLES = 0x0208;
+    public static final int TAG_JPEG_AC_TABLES = 0x0209;
 
     public static final int TAG_YCBCR_COEFFICIENTS                = 0x0211;
@@ -266,4 +278,9 @@
     public static final int TAG_METERING_MODE                     = 0x9207;
 
+    /**
+     * @deprecated use {@link com.drew.metadata.exif.ExifDirectoryBase#TAG_WHITE_BALANCE} instead.
+     */
+    @Deprecated
+    public static final int TAG_LIGHT_SOURCE                      = 0x9208;
     /**
      * White balance (aka light source). '0' means unknown, '1' daylight,
@@ -574,5 +591,5 @@
     public static final int TAG_GAMMA                             = 0xA500;
 
-    public static final int TAG_PRINT_IM                          = 0xC4A5;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO         = 0xC4A5;
 
     public static final int TAG_PANASONIC_TITLE                   = 0xC6D2;
@@ -612,4 +629,5 @@
         map.put(TAG_PAGE_NAME, "Page Name");
         map.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
+        map.put(TAG_PAGE_NUMBER, "Page Number");
         map.put(TAG_TRANSFER_FUNCTION, "Transfer Function");
         map.put(TAG_SOFTWARE, "Software");
@@ -628,4 +646,12 @@
         map.put(TAG_JPEG_TABLES, "JPEG Tables");
         map.put(TAG_JPEG_PROC, "JPEG Proc");
+
+        map.put(TAG_JPEG_RESTART_INTERVAL, "JPEG Restart Interval");
+        map.put(TAG_JPEG_LOSSLESS_PREDICTORS, "JPEG Lossless Predictors");
+        map.put(TAG_JPEG_POINT_TRANSFORMS, "JPEG Point Transforms");
+        map.put(TAG_JPEG_Q_TABLES, "JPEGQ Tables");
+        map.put(TAG_JPEG_DC_TABLES, "JPEGDC Tables");
+        map.put(TAG_JPEG_AC_TABLES, "JPEGAC Tables");
+
         map.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
         map.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
@@ -730,5 +756,5 @@
         map.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
         map.put(TAG_GAMMA, "Gamma");
-        map.put(TAG_PRINT_IM, "Print IM");
+        map.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
         map.put(TAG_PANASONIC_TITLE, "Panasonic Title");
         map.put(TAG_PANASONIC_TITLE_2, "Panasonic Title (2)");
Index: /trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,4 +29,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifIFD0Descriptor extends ExifDescriptorBase<ExifIFD0Directory>
 {
Index: /trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifIFD0Directory extends ExifDirectoryBase
 {
Index: /trunk/src/com/drew/metadata/exif/ExifImageDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifImageDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/ExifImageDescriptor.java	(revision 13061)
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ExifImageDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ExifImageDescriptor extends ExifDescriptorBase<ExifImageDirectory>
+{
+    public ExifImageDescriptor(@NotNull ExifImageDirectory directory)
+    {
+        super(directory);
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/ExifImageDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifImageDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/ExifImageDirectory.java	(revision 13061)
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.util.HashMap;
+
+/**
+ * Describes One of several Exif directories.
+ *
+ * Holds information about image IFD's in a chain after the first. The first page is stored in IFD0.
+ * Currently, this only applied to multi-page TIFF images
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ExifImageDirectory extends ExifDirectoryBase
+{
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+    }
+
+    public ExifImageDirectory()
+    {
+        this.setDescriptor(new ExifImageDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Exif Image";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,4 +28,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifInteropDescriptor extends ExifDescriptorBase<ExifInteropDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,4 +30,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifInteropDirectory extends ExifDirectoryBase
 {
Index: /trunk/src/com/drew/metadata/exif/ExifReader.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -42,20 +42,9 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifReader implements JpegSegmentMetadataReader
 {
     /** Exif data stored in JPEG files' APP1 segment are preceded by this six character preamble. */
     public static final String JPEG_SEGMENT_PREAMBLE = "Exif\0\0";
-
-    private boolean _storeThumbnailBytes = true;
-
-    public boolean isStoreThumbnailBytes()
-    {
-        return _storeThumbnailBytes;
-    }
-
-    public void setStoreThumbnailBytes(boolean storeThumbnailBytes)
-    {
-        _storeThumbnailBytes = storeThumbnailBytes;
-    }
 
     @NotNull
@@ -89,18 +78,22 @@
     }
 
-    /** Reads TIFF formatted Exif data a specified offset within a {@link RandomAccessReader}. */
+    /** Reads TIFF formatted Exif data at a specified offset within a {@link RandomAccessReader}. */
     public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset, @Nullable Directory parentDirectory)
     {
+        ExifTiffHandler exifTiffHandler = new ExifTiffHandler(metadata, parentDirectory);
+
         try {
             // Read the TIFF-formatted Exif data
             new TiffReader().processTiff(
                 reader,
-                new ExifTiffHandler(metadata, _storeThumbnailBytes, parentDirectory),
+                exifTiffHandler,
                 readerOffset
             );
         } catch (TiffProcessingException e) {
+            exifTiffHandler.error("Exception processing TIFF data: " + e.getMessage());
             // TODO what do to with this error state?
             e.printStackTrace(System.err);
         } catch (IOException e) {
+            exifTiffHandler.error("Exception processing TIFF data: " + e.getMessage());
             // TODO what do to with this error state?
             e.printStackTrace(System.err);
Index: /trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,4 +28,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifSubIFDDescriptor extends ExifDescriptorBase<ExifSubIFDDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,10 +21,10 @@
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
 import java.util.Date;
 import java.util.HashMap;
 import java.util.TimeZone;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
 
 /**
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifSubIFDDirectory extends ExifDirectoryBase
 {
@@ -88,5 +89,5 @@
      */
     @Nullable
-    public Date getDateOriginal(TimeZone timeZone)
+    public Date getDateOriginal(@Nullable TimeZone timeZone)
     {
         return getDate(TAG_DATETIME_ORIGINAL, getString(TAG_SUBSECOND_TIME_ORIGINAL), timeZone);
@@ -116,5 +117,5 @@
      */
     @Nullable
-    public Date getDateDigitized(TimeZone timeZone)
+    public Date getDateDigitized(@Nullable 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 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifThumbnailDescriptor extends ExifDescriptorBase<ExifThumbnailDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,11 +22,7 @@
 package com.drew.metadata.exif;
 
-import java.io.FileOutputStream;
-import java.io.IOException;
+import com.drew.lang.annotations.NotNull;
+
 import java.util.HashMap;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.MetadataException;
 
 /**
@@ -35,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifThumbnailDirectory extends ExifDirectoryBase
 {
@@ -46,4 +43,10 @@
     public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
 
+    /**
+     * @deprecated use {@link com.drew.metadata.exif.ExifDirectoryBase#TAG_COMPRESSION} instead.
+     */
+    @Deprecated
+    public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103;
+
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
@@ -56,7 +59,4 @@
         _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
     }
-
-    @Nullable
-    private byte[] _thumbnailData;
 
     public ExifThumbnailDirectory()
@@ -78,225 +78,3 @@
         return _tagNameMap;
     }
-
-    public boolean hasThumbnailData()
-    {
-        return _thumbnailData != null;
-    }
-
-    @Nullable
-    public byte[] getThumbnailData()
-    {
-        return _thumbnailData;
-    }
-
-    public void setThumbnailData(@Nullable byte[] data)
-    {
-        _thumbnailData = data;
-    }
-
-    public void writeThumbnail(@NotNull String filename) throws MetadataException, IOException
-    {
-        byte[] data = _thumbnailData;
-
-        if (data == null)
-            throw new MetadataException("No thumbnail data exists.");
-
-        FileOutputStream stream = null;
-        try {
-            stream = new FileOutputStream(filename);
-            stream.write(data);
-        } finally {
-            if (stream != null)
-                stream.close();
-        }
-    }
-
-/*
-    // This thumbnail extraction code is not complete, and is included to assist anyone who feels like looking into
-    // it.  Please share any progress with the original author, and hence the community.  Thanks.
-
-    public Image getThumbnailImage() throws MetadataException
-    {
-        if (!hasThumbnailData())
-            return null;
-
-        int compression = 0;
-        try {
-            compression = this.getInt(ExifSubIFDDirectory.TAG_COMPRESSION);
-        } catch (Throwable e) {
-            this.addError("Unable to determine thumbnail type " + e.getMessage());
-        }
-
-        final byte[] thumbnailBytes = getThumbnailData();
-
-        if (compression == ExifSubIFDDirectory.COMPRESSION_JPEG)
-        {
-            // JPEG Thumbnail
-            // operate directly on thumbnailBytes
-            return decodeBytesAsImage(thumbnailBytes);
-        }
-        else if (compression == ExifSubIFDDirectory.COMPRESSION_NONE)
-        {
-            // uncompressed thumbnail (raw RGB data)
-            if (!this.containsTag(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION))
-                return null;
-
-            try
-            {
-                // If the image is RGB format, then convert it to a bitmap
-                final int photometricInterpretation = this.getInt(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
-                if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_RGB)
-                {
-                    // RGB
-                    Image image = createImageFromRawRgb(thumbnailBytes);
-                    return image;
-                }
-                else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_YCBCR)
-                {
-                    // YCbCr
-                    Image image = createImageFromRawYCbCr(thumbnailBytes);
-                    return image;
-                }
-                else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_MONOCHROME)
-                {
-                    // Monochrome
-                    return null;
-                }
-            } catch (Throwable e) {
-                this.addError("Unable to extract thumbnail: " + e.getMessage());
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Handle the YCbCr thumbnail encoding used by Ricoh RDC4200/4300, Fuji DS-7/300 and DX-5/7/9 cameras.
-     *
-     * At DX-5/7/9, YCbCrSubsampling(0x0212) has values of '2,1', PlanarConfiguration(0x011c) has a value '1'. So the
-     * data align of this image is below.
-     *
-     * Y(0,0),Y(1,0),Cb(0,0),Cr(0,0), Y(2,0),Y(3,0),Cb(2,0),Cr(3.0), Y(4,0),Y(5,0),Cb(4,0),Cr(4,0). . . .
-     *
-     * The numbers in parenthesis are pixel coordinates. DX series' YCbCrCoefficients(0x0211) has values '0.299/0.587/0.114',
-     * ReferenceBlackWhite(0x0214) has values '0,255,128,255,128,255'. Therefore to convert from Y/Cb/Cr to RGB is;
-     *
-     * B(0,0)=(Cb-128)*(2-0.114*2)+Y(0,0)
-     * R(0,0)=(Cr-128)*(2-0.299*2)+Y(0,0)
-     * G(0,0)=(Y(0,0)-0.114*B(0,0)-0.299*R(0,0))/0.587
-     *
-     * Horizontal subsampling is a value '2', so you can calculate B(1,0)/R(1,0)/G(1,0) by using the Y(1,0) and Cr(0,0)/Cb(0,0).
-     * Repeat this conversion by value of ImageWidth(0x0100) and ImageLength(0x0101).
-     *
-     * @param thumbnailBytes
-     * @return
-     * @throws com.drew.metadata.MetadataException
-     * /
-    private Image createImageFromRawYCbCr(byte[] thumbnailBytes) throws MetadataException
-    {
-        /*
-            Y  =  0.257R + 0.504G + 0.098B + 16
-            Cb = -0.148R - 0.291G + 0.439B + 128
-            Cr =  0.439R - 0.368G - 0.071B + 128
-
-            G = 1.164(Y-16) - 0.391(Cb-128) - 0.813(Cr-128)
-            R = 1.164(Y-16) + 1.596(Cr-128)
-            B = 1.164(Y-16) + 2.018(Cb-128)
-
-            R, G and B range from 0 to 255.
-            Y ranges from 16 to 235.
-            Cb and Cr range from 16 to 240.
-
-            http://www.faqs.org/faqs/graphics/colorspace-faq/
-        * /
-
-        int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
-        final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
-        final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
-//        final int headerLength = 54;
-//        byte[] result = new byte[length + headerLength];
-//        // Add a windows BMP header described:
-//        // http://www.onicos.com/staff/iz/formats/bmp.html
-//        result[0] = 'B';
-//        result[1] = 'M'; // File Type identifier
-//        result[3] = (byte)(result.length / 256);
-//        result[2] = (byte)result.length;
-//        result[10] = (byte)headerLength;
-//        result[14] = 40; // MS Windows BMP header
-//        result[18] = (byte)imageWidth;
-//        result[22] = (byte)imageHeight;
-//        result[26] = 1;  // 1 Plane
-//        result[28] = 24; // Colour depth
-//        result[34] = (byte)length;
-//        result[35] = (byte)(length / 256);
-
-        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
-
-        // order is YCbCr and image is upside down, bitmaps are BGR
-////        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
-//        {
-//            final int y =  thumbnailBytes[dataOffset - 2] & 0xFF;
-//            final int cb = thumbnailBytes[dataOffset - 1] & 0xFF;
-//            final int cr = thumbnailBytes[dataOffset] & 0xFF;
-//            if (y<16 || y>235 || cb<16 || cb>240 || cr<16 || cr>240)
-//                "".toString();
-//
-//            int g = (int)(1.164*(y-16) - 0.391*(cb-128) - 0.813*(cr-128));
-//            int r = (int)(1.164*(y-16) + 1.596*(cr-128));
-//            int b = (int)(1.164*(y-16) + 2.018*(cb-128));
-//
-////            result[i] = (byte)b;
-////            result[i + 1] = (byte)g;
-////            result[i + 2] = (byte)r;
-//
-//            // TODO compose the image here
-//            image.setRGB(1, 2, 3);
-//        }
-
-        return image;
-    }
-
-    /**
-     * Creates a thumbnail image in (Windows) BMP format from raw RGB data.
-     * @param thumbnailBytes
-     * @return
-     * @throws com.drew.metadata.MetadataException
-     * /
-    private Image createImageFromRawRgb(byte[] thumbnailBytes) throws MetadataException
-    {
-        final int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
-        final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
-        final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
-//        final int headerLength = 54;
-//        final byte[] result = new byte[length + headerLength];
-//        // Add a windows BMP header described:
-//        // http://www.onicos.com/staff/iz/formats/bmp.html
-//        result[0] = 'B';
-//        result[1] = 'M'; // File Type identifier
-//        result[3] = (byte)(result.length / 256);
-//        result[2] = (byte)result.length;
-//        result[10] = (byte)headerLength;
-//        result[14] = 40; // MS Windows BMP header
-//        result[18] = (byte)imageWidth;
-//        result[22] = (byte)imageHeight;
-//        result[26] = 1;  // 1 Plane
-//        result[28] = 24; // Colour depth
-//        result[34] = (byte)length;
-//        result[35] = (byte)(length / 256);
-
-        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
-
-        // order is RGB and image is upside down, bitmaps are BGR
-//        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
-//        {
-//            byte b = thumbnailBytes[dataOffset - 2];
-//            byte g = thumbnailBytes[dataOffset - 1];
-//            byte r = thumbnailBytes[dataOffset];
-//
-//            // TODO compose the image here
-//            image.setRGB(1, 2, 3);
-//        }
-
-        return image;
-    }
-*/
 }
Index: /trunk/src/com/drew/metadata/exif/ExifTiffHandler.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,6 +21,14 @@
 package com.drew.metadata.exif;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.imaging.jpeg.JpegProcessingException;
 import com.drew.imaging.tiff.TiffProcessingException;
 import com.drew.imaging.tiff.TiffReader;
+import com.drew.lang.Charsets;
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.SequentialByteArrayReader;
@@ -29,10 +37,8 @@
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 import com.drew.metadata.exif.makernotes.*;
 import com.drew.metadata.iptc.IptcReader;
 import com.drew.metadata.tiff.DirectoryTiffHandler;
-
-import java.io.IOException;
-import java.util.Set;
 
 /**
@@ -46,10 +52,7 @@
 public class ExifTiffHandler extends DirectoryTiffHandler
 {
-    private final boolean _storeThumbnailBytes;
-
-    public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes, @Nullable Directory parentDirectory)
-    {
-        super(metadata, ExifIFD0Directory.class);
-        _storeThumbnailBytes = storeThumbnailBytes;
+    public ExifTiffHandler(@NotNull Metadata metadata, @Nullable Directory parentDirectory)
+    {
+        super(metadata);
 
         if (parentDirectory != null)
@@ -64,6 +67,16 @@
         final int panasonicRawTiffMarker = 0x0055; // for RW2 files
 
-        if (marker != standardTiffMarker && marker != olympusRawTiffMarker && marker != olympusRawTiffMarker2 && marker != panasonicRawTiffMarker) {
-            throw new TiffProcessingException("Unexpected TIFF marker: 0x" + Integer.toHexString(marker));
+        switch (marker)
+        {
+            case standardTiffMarker:
+            case olympusRawTiffMarker:      // Todo: implement an IFD0, if there is one
+            case olympusRawTiffMarker2:     // Todo: implement an IFD0, if there is one
+                pushDirectory(ExifIFD0Directory.class);
+                break;
+            case panasonicRawTiffMarker:
+                pushDirectory(PanasonicRawIFD0Directory.class);
+                break;
+            default:
+                throw new TiffProcessingException(String.format("Unexpected TIFF marker: 0x%X", marker));
         }
     }
@@ -76,5 +89,5 @@
         }
 
-        if (_currentDirectory instanceof ExifIFD0Directory) {
+        if (_currentDirectory instanceof ExifIFD0Directory || _currentDirectory instanceof PanasonicRawIFD0Directory) {
             if (tagId == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET) {
                 pushDirectory(ExifSubIFDDirectory.class);
@@ -96,12 +109,31 @@
 
         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;
+            // Note: these also appear in customProcessTag because some are IFD pointers while others begin immediately
+            // for the same directories
+            switch(tagId) {
+                case OlympusMakernoteDirectory.TAG_EQUIPMENT:
+                    pushDirectory(OlympusEquipmentMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS:
+                    pushDirectory(OlympusCameraSettingsMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT:
+                    pushDirectory(OlympusRawDevelopmentMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT_2:
+                    pushDirectory(OlympusRawDevelopment2MakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_IMAGE_PROCESSING:
+                    pushDirectory(OlympusImageProcessingMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_FOCUS_INFO:
+                    pushDirectory(OlympusFocusInfoMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_INFO:
+                    pushDirectory(OlympusRawInfoMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_MAIN_INFO:
+                    pushDirectory(OlympusMakernoteDirectory.class);
+                    return true;
             }
         }
@@ -113,6 +145,12 @@
     {
         // In Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case.
-        if (_currentDirectory instanceof ExifIFD0Directory) {
-            pushDirectory(ExifThumbnailDirectory.class);
+        // UPDATE: In multipage TIFFs, the 'follower' IFD points to the next image in the set
+        if (_currentDirectory instanceof ExifIFD0Directory || _currentDirectory instanceof ExifImageDirectory) {
+            // If the PageNumber tag is defined, assume this is a multipage TIFF or similar
+            // TODO: Find better ways to know which follower Directory should be used
+            if (_currentDirectory.containsTag(ExifDirectoryBase.TAG_PAGE_NUMBER))
+                pushDirectory(ExifImageDirectory.class);
+            else
+                pushDirectory(ExifThumbnailDirectory.class);
             return true;
         }
@@ -132,4 +170,8 @@
         if (formatCode == 13)
             return componentCount * 4;
+
+        // an unknown (0) formatCode needs to be potentially handled later as a highly custom directory tag
+        if(formatCode == 0)
+            return 0L;
 
         return null;
@@ -143,4 +185,18 @@
                                     final int byteCount) throws IOException
     {
+        // Some 0x0000 tags have a 0 byteCount. Determine whether it's bad.
+        if (tagId == 0)
+        {
+            if (_currentDirectory.containsTag(tagId))
+            {
+                // Let it go through for now. Some directories handle it, some don't
+                return false;
+            }
+
+            // Skip over 0x0000 tags that don't have any associated bytes. No idea what it contains in this case, if anything.
+            if (byteCount == 0)
+                return true;
+        }
+
         // Custom processing for the Makernote tag
         if (tagId == ExifSubIFDDirectory.TAG_MAKERNOTE && _currentDirectory instanceof ExifSubIFDDirectory) {
@@ -159,22 +215,143 @@
         }
 
+        if (HandlePrintIM(_currentDirectory, tagId))
+        {
+            PrintIMDirectory printIMDirectory = new PrintIMDirectory();
+            printIMDirectory.setParent(_currentDirectory);
+            _metadata.addDirectory(printIMDirectory);
+            ProcessPrintIM(printIMDirectory, tagOffset, reader, byteCount);
+            return true;
+        }
+
+        // Note: these also appear in tryEnterSubIfd because some are IFD pointers while others begin immediately
+        // for the same directories
+        if(_currentDirectory instanceof OlympusMakernoteDirectory)
+        {
+            switch (tagId)
+            {
+                case OlympusMakernoteDirectory.TAG_EQUIPMENT:
+                    pushDirectory(OlympusEquipmentMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS:
+                    pushDirectory(OlympusCameraSettingsMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT:
+                    pushDirectory(OlympusRawDevelopmentMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT_2:
+                    pushDirectory(OlympusRawDevelopment2MakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_IMAGE_PROCESSING:
+                    pushDirectory(OlympusImageProcessingMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_FOCUS_INFO:
+                    pushDirectory(OlympusFocusInfoMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_INFO:
+                    pushDirectory(OlympusRawInfoMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_MAIN_INFO:
+                    pushDirectory(OlympusMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+            }
+        }
+
+        if (_currentDirectory instanceof PanasonicRawIFD0Directory)
+        {
+            // these contain binary data with specific offsets, and can't be processed as regular ifd's.
+            // The binary data is broken into 'fake' tags and there is a pattern.
+            switch (tagId)
+            {
+                case PanasonicRawIFD0Directory.TagWbInfo:
+                    PanasonicRawWbInfoDirectory dirWbInfo = new PanasonicRawWbInfoDirectory();
+                    dirWbInfo.setParent(_currentDirectory);
+                    _metadata.addDirectory(dirWbInfo);
+                    ProcessBinary(dirWbInfo, tagOffset, reader, byteCount, false, 2);
+                    return true;
+                case PanasonicRawIFD0Directory.TagWbInfo2:
+                    PanasonicRawWbInfo2Directory dirWbInfo2 = new PanasonicRawWbInfo2Directory();
+                    dirWbInfo2.setParent(_currentDirectory);
+                    _metadata.addDirectory(dirWbInfo2);
+                    ProcessBinary(dirWbInfo2, tagOffset, reader, byteCount, false, 3);
+                    return true;
+                case PanasonicRawIFD0Directory.TagDistortionInfo:
+                    PanasonicRawDistortionDirectory dirDistort = new PanasonicRawDistortionDirectory();
+                    dirDistort.setParent(_currentDirectory);
+                    _metadata.addDirectory(dirDistort);
+                    ProcessBinary(dirDistort, tagOffset, reader, byteCount, true, 1);
+                    return true;
+            }
+        }
+
+        // Panasonic RAW sometimes contains an embedded version of the data as a JPG file.
+        if (tagId == PanasonicRawIFD0Directory.TagJpgFromRaw && _currentDirectory instanceof PanasonicRawIFD0Directory)
+        {
+            byte[] jpegrawbytes = reader.getBytes(tagOffset, byteCount);
+
+            // Extract information from embedded image since it is metadata-rich
+            ByteArrayInputStream jpegmem = new ByteArrayInputStream(jpegrawbytes);
+            try {
+                Metadata jpegDirectory = JpegMetadataReader.readMetadata(jpegmem);
+                for (Directory directory : jpegDirectory.getDirectories()) {
+                    directory.setParent(_currentDirectory);
+                    _metadata.addDirectory(directory);
+                }
+                return true;
+            } catch (JpegProcessingException e) {
+                _currentDirectory.addError("Error processing JpgFromRaw: " + e.getMessage());
+            } catch (IOException e) {
+                _currentDirectory.addError("Error reading JpgFromRaw: " + e.getMessage());
+            }
+        }
+
         return false;
     }
 
-    public void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset)
-    {
-        if (_storeThumbnailBytes) {
-            // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
-            ExifThumbnailDirectory thumbnailDirectory = _metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
-            if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_COMPRESSION)) {
-                Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
-                Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
-                if (offset != null && length != null) {
-                    try {
-                        byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);
-                        thumbnailDirectory.setThumbnailData(thumbnailData);
-                    } catch (IOException ex) {
-                        thumbnailDirectory.addError("Invalid thumbnail data specification: " + ex.getMessage());
+    private static void ProcessBinary(@NotNull final Directory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader, final int byteCount, final Boolean issigned, final int arrayLength) throws IOException
+    {
+        // expects signed/unsigned int16 (for now)
+        //int byteSize = issigned ? sizeof(short) : sizeof(ushort);
+        int byteSize = 2;
+
+        // 'directory' is assumed to contain tags that correspond to the byte position unless it's a set of bytes
+        for (int i = 0; i < byteCount; i++)
+        {
+            if (directory.hasTagName(i))
+            {
+                // only process this tag if the 'next' integral tag exists. Otherwise, it's a set of bytes
+                if (i < byteCount - 1 && directory.hasTagName(i + 1))
+                {
+                    if(issigned)
+                        directory.setObject(i, reader.getInt16(tagValueOffset + (i* byteSize)));
+                    else
+                        directory.setObject(i, reader.getUInt16(tagValueOffset + (i* byteSize)));
+                }
+                else
+                {
+                    // the next arrayLength bytes are a multi-byte value
+                    if (issigned)
+                    {
+                        short[] val = new short[arrayLength];
+                        for (int j = 0; j<val.length; j++)
+                            val[j] = reader.getInt16(tagValueOffset + ((i + j) * byteSize));
+                        directory.setObjectArray(i, val);
                     }
+                    else
+                    {
+                        int[] val = new int[arrayLength];
+                        for (int j = 0; j<val.length; j++)
+                            val[j] = reader.getUInt16(tagValueOffset + ((i + j) * byteSize));
+                        directory.setObjectArray(i, val);
+                    }
+
+                    i += arrayLength - 1;
                 }
             }
@@ -190,18 +367,16 @@
         Directory ifd0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
 
-        if (ifd0Directory == null)
-            return false;
-
-        String cameraMake = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
-
-        final String firstTwoChars = reader.getString(makernoteOffset, 2);
-        final String firstThreeChars = reader.getString(makernoteOffset, 3);
-        final String firstFourChars = reader.getString(makernoteOffset, 4);
-        final String firstFiveChars = reader.getString(makernoteOffset, 5);
-        final String firstSixChars = reader.getString(makernoteOffset, 6);
-        final String firstSevenChars = reader.getString(makernoteOffset, 7);
-        final String firstEightChars = reader.getString(makernoteOffset, 8);
-        final String firstTenChars = reader.getString(makernoteOffset, 10);
-        final String firstTwelveChars = reader.getString(makernoteOffset, 12);
+        String cameraMake = ifd0Directory == null ? null : ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
+
+        final String firstTwoChars    = reader.getString(makernoteOffset, 2, Charsets.UTF_8);
+        final String firstThreeChars  = reader.getString(makernoteOffset, 3, Charsets.UTF_8);
+        final String firstFourChars   = reader.getString(makernoteOffset, 4, Charsets.UTF_8);
+        final String firstFiveChars   = reader.getString(makernoteOffset, 5, Charsets.UTF_8);
+        final String firstSixChars    = reader.getString(makernoteOffset, 6, Charsets.UTF_8);
+        final String firstSevenChars  = reader.getString(makernoteOffset, 7, Charsets.UTF_8);
+        final String firstEightChars  = reader.getString(makernoteOffset, 8, Charsets.UTF_8);
+        final String firstNineChars   = reader.getString(makernoteOffset, 9, Charsets.UTF_8);
+        final String firstTenChars    = reader.getString(makernoteOffset, 10, Charsets.UTF_8);
+        final String firstTwelveChars = reader.getString(makernoteOffset, 12, Charsets.UTF_8);
 
         boolean byteOrderBefore = reader.isMotorolaByteOrder();
@@ -243,5 +418,5 @@
                         break;
                     default:
-                        ifd0Directory.addError("Unsupported Nikon makernote data ignored.");
+                        _currentDirectory.addError("Unsupported Nikon makernote data ignored.");
                         break;
                 }
@@ -254,4 +429,10 @@
             pushDirectory(SonyType1MakernoteDirectory.class);
             TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset);
+        // Do this check LAST after most other Sony checks
+        } else if (cameraMake != null && cameraMake.startsWith("SONY") &&
+                !Arrays.equals(reader.getBytes(makernoteOffset, 2), new byte[]{ 0x01, 0x00 }) ) {
+            // The IFD begins with the first Makernote byte (no ASCII name). Used in SR2 and ARW images
+            pushDirectory(SonyType1MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
         } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
             // force MM for this directory
@@ -294,5 +475,21 @@
         } else if ("LEICA".equals(firstFiveChars)) {
             reader.setMotorolaByteOrder(false);
-            if ("Leica Camera AG".equals(cameraMake)) {
+
+            // used by the X1/X2/X VARIO/T
+            // (X1 starts with "LEICA\0\x01\0", Make is "LEICA CAMERA AG")
+            // (X2 starts with "LEICA\0\x05\0", Make is "LEICA CAMERA AG")
+            // (X VARIO starts with "LEICA\0\x04\0", Make is "LEICA CAMERA AG")
+            // (T (Typ 701) starts with "LEICA\0\0x6", Make is "LEICA CAMERA AG")
+            // (X (Typ 113) starts with "LEICA\0\0x7", Make is "LEICA CAMERA AG")
+
+            if ("LEICA\0\u0001\0".equals(firstEightChars) ||
+                "LEICA\0\u0004\0".equals(firstEightChars) ||
+                "LEICA\0\u0005\0".equals(firstEightChars) ||
+                "LEICA\0\u0006\0".equals(firstEightChars) ||
+                "LEICA\0\u0007\0".equals(firstEightChars))
+            {
+                pushDirectory(LeicaType5MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
+            } else if ("Leica Camera AG".equals(cameraMake)) {
                 pushDirectory(LeicaMakernoteDirectory.class);
                 TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
@@ -304,5 +501,5 @@
                 return false;
             }
-        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12))) {
+        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12, Charsets.UTF_8))) {
             // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD
             // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment
@@ -349,4 +546,23 @@
                 TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
             }
+        } else if (firstTenChars.equals("Apple iOS\0")) {
+            // Always in Motorola byte order
+            boolean orderBefore = reader.isMotorolaByteOrder();
+            reader.setMotorolaByteOrder(true);
+            pushDirectory(AppleMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 14, makernoteOffset);
+            reader.setMotorolaByteOrder(orderBefore);
+        } else if (reader.getUInt16(makernoteOffset) == ReconyxHyperFireMakernoteDirectory.MAKERNOTE_VERSION) {
+            ReconyxHyperFireMakernoteDirectory directory = new ReconyxHyperFireMakernoteDirectory();
+            _metadata.addDirectory(directory);
+            processReconyxHyperFireMakernote(directory, makernoteOffset, reader);
+        } else if (firstNineChars.equalsIgnoreCase("RECONYXUF")) {
+            ReconyxUltraFireMakernoteDirectory directory = new ReconyxUltraFireMakernoteDirectory();
+            _metadata.addDirectory(directory);
+            processReconyxUltraFireMakernote(directory, makernoteOffset, reader);
+        } else if ("SAMSUNG".equals(cameraMake)) {
+            // Only handles Type2 notes correctly. Others aren't implemented, and it's complex to determine which ones to use
+            pushDirectory(SamsungType2MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
         } else {
             // The makernote is not comprehended by this library.
@@ -359,4 +575,89 @@
     }
 
+    private static Boolean HandlePrintIM(@NotNull final Directory directory, final int tagId)
+    {
+        if (tagId == ExifDirectoryBase.TAG_PRINT_IMAGE_MATCHING_INFO)
+            return true;
+
+        if (tagId == 0x0E00)
+        {
+            // Tempting to say every tagid of 0x0E00 is a PIM tag, but can't be 100% sure
+            if (directory instanceof CasioType2MakernoteDirectory ||
+                directory instanceof KyoceraMakernoteDirectory ||
+                directory instanceof NikonType2MakernoteDirectory ||
+                directory instanceof OlympusMakernoteDirectory ||
+                directory instanceof PanasonicMakernoteDirectory ||
+                directory instanceof PentaxMakernoteDirectory ||
+                directory instanceof RicohMakernoteDirectory ||
+                directory instanceof SanyoMakernoteDirectory ||
+                directory instanceof SonyType1MakernoteDirectory)
+                return true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Process PrintIM IFD
+    /// </summary>
+    /// <remarks>
+    /// Converted from Exiftool version 10.33 created by Phil Harvey
+    /// http://www.sno.phy.queensu.ca/~phil/exiftool/
+    /// lib\Image\ExifTool\PrintIM.pm
+    /// </remarks>
+    private static void ProcessPrintIM(@NotNull final PrintIMDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader, final int byteCount) throws IOException
+    {
+        Boolean resetByteOrder = null;
+
+        if (byteCount == 0)
+        {
+            directory.addError("Empty PrintIM data");
+            return;
+        }
+
+        if (byteCount <= 15)
+        {
+            directory.addError("Bad PrintIM data");
+            return;
+        }
+
+        String header = reader.getString(tagValueOffset, 12, Charsets.UTF_8);
+
+        if (!header.startsWith("PrintIM")) //, StringComparison.Ordinal))
+        {
+            directory.addError("Invalid PrintIM header");
+            return;
+        }
+
+        // check size of PrintIM block
+        int num = reader.getUInt16(tagValueOffset + 14);
+        if (byteCount < 16 + num * 6)
+        {
+            // size is too big, maybe byte ordering is wrong
+            resetByteOrder = reader.isMotorolaByteOrder();
+            reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder());
+            num = reader.getUInt16(tagValueOffset + 14);
+            if (byteCount < 16 + num * 6)
+            {
+                directory.addError("Bad PrintIM size");
+                return;
+            }
+        }
+
+        directory.setObject(PrintIMDirectory.TagPrintImVersion, header.substring(8, 12));
+
+        for (int n = 0; n < num; n++)
+        {
+            int pos = tagValueOffset + 16 + n * 6;
+            int tag = reader.getUInt16(pos);
+            long val = reader.getUInt32(pos + 2);
+
+            directory.setObject(tag, val);
+        }
+
+        if (resetByteOrder != null)
+            reader.setMotorolaByteOrder(resetByteOrder);
+    }
+
     private static void processKodakMakernote(@NotNull final KodakMakernoteDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader)
     {
@@ -364,5 +665,5 @@
         int dataOffset = tagValueOffset + 8;
         try {
-            directory.setString(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getString(dataOffset, 8));
+            directory.setStringValue(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getStringValue(dataOffset, 8, Charsets.UTF_8));
             directory.setInt(KodakMakernoteDirectory.TAG_QUALITY, reader.getUInt8(dataOffset + 9));
             directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE, reader.getUInt8(dataOffset + 10));
@@ -394,4 +695,147 @@
         }
     }
+
+    private static void processReconyxHyperFireMakernote(@NotNull final ReconyxHyperFireMakernoteDirectory directory, final int makernoteOffset, @NotNull final RandomAccessReader reader) throws IOException
+    {
+        directory.setObject(ReconyxHyperFireMakernoteDirectory.TAG_MAKERNOTE_VERSION, reader.getUInt16(makernoteOffset));
+
+        int major = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION);
+        int minor = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 2);
+        int revision = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 4);
+        String buildYear = String.format("%04X", reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 6));
+        String buildDate = String.format("%04X", reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 8));
+        String buildYearAndDate = buildYear + buildDate;
+        Integer build;
+        try {
+            build = Integer.parseInt(buildYearAndDate);
+        } catch (NumberFormatException e) {
+            build = null;
+        }
+        if (build != null)
+        {
+            directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d.%s", major, minor, revision, build));
+        }
+        else
+        {
+            directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d", major, minor, revision));
+            directory.addError("Error processing Reconyx HyperFire makernote data: build '" + buildYearAndDate + "' is not in the expected format and will be omitted from Firmware Version.");
+        }
+
+        directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_TRIGGER_MODE, String.valueOf((char)reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_TRIGGER_MODE)));
+        directory.setIntArray(ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE,
+                      new int[]
+                      {
+                          reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE),
+                          reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE + 2)
+                      });
+
+        int eventNumberHigh = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER);
+        int eventNumberLow = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER + 2);
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER, (eventNumberHigh << 16) + eventNumberLow);
+
+        int seconds = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL);
+        int minutes = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 2);
+        int hour = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 4);
+        int month = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 6);
+        int day = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 8);
+        int year = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 10);
+
+        if ((seconds >= 0 && seconds < 60) &&
+            (minutes >= 0 && minutes < 60) &&
+            (hour >= 0 && hour < 24) &&
+            (month >= 1 && month < 13) &&
+            (day >= 1 && day < 32) &&
+            (year >= 1 && year <= 9999))
+        {
+            directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL,
+                    String.format("%4d:%2d:%2d %2d:%2d:%2d", year, month, day, hour, minutes, seconds));
+        }
+        else
+        {
+            directory.addError("Error processing Reconyx HyperFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time.");
+        }
+
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_MOON_PHASE, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_MOON_PHASE));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, reader.getInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE_FAHRENHEIT));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE, reader.getInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE));
+        //directory.setByteArray(ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, reader.getBytes(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, 28));
+        directory.setStringValue(ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, new StringValue(reader.getBytes(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, 28), Charsets.UTF_16LE));
+        // two unread bytes: the serial number's terminating null
+
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_CONTRAST, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_CONTRAST));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_BRIGHTNESS, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_BRIGHTNESS));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_SHARPNESS, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SHARPNESS));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_SATURATION, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SATURATION));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_INFRARED_ILLUMINATOR, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_INFRARED_ILLUMINATOR));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_MOTION_SENSITIVITY, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_MOTION_SENSITIVITY));
+        directory.setDouble(ReconyxHyperFireMakernoteDirectory.TAG_BATTERY_VOLTAGE, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_BATTERY_VOLTAGE) / 1000.0);
+        directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_USER_LABEL, reader.getNullTerminatedString(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_USER_LABEL, 44, Charsets.UTF_8));
+    }
+
+    private static void processReconyxUltraFireMakernote(@NotNull final ReconyxUltraFireMakernoteDirectory directory, final int makernoteOffset, @NotNull final RandomAccessReader reader) throws IOException
+    {
+        directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_LABEL, reader.getString(makernoteOffset, 9, Charsets.UTF_8));
+        /*uint makernoteID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteID));
+        directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteID, makernoteID);
+        if (makernoteID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_ID)
+        {
+            directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote ID 0x" + makernoteID.ToString("x8"));
+            return;
+        }
+        directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteSize, ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteSize)));
+        uint makernotePublicID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID));
+        directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID, makernotePublicID);
+        if (makernotePublicID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_PUBLIC_ID)
+        {
+            directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote Public ID 0x" + makernotePublicID.ToString("x8"));
+            return;
+        }*/
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicSize, ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicSize)));
+
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagCameraVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagCameraVersion, reader));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagUibVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagUibVersion, reader));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagBtlVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagBtlVersion, reader));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagPexVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagPexVersion, reader));
+
+        directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_EVENT_TYPE, reader.getString(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_EVENT_TYPE, 1, Charsets.UTF_8));
+        directory.setIntArray(ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE,
+                      new int[]
+                      {
+                          reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE),
+                          reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE + 1)
+                      });
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagEventNumber, ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagEventNumber)));
+
+        byte seconds = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL);
+        byte minutes = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 1);
+        byte hour = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 2);
+        byte day = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 3);
+        byte month = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 4);
+        /*ushort year = ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagDateTimeOriginal + 5));
+        if ((seconds >= 0 && seconds < 60) &&
+            (minutes >= 0 && minutes < 60) &&
+            (hour >= 0 && hour < 24) &&
+            (month >= 1 && month < 13) &&
+            (day >= 1 && day < 32) &&
+            (year >= 1 && year <= 9999))
+        {
+            directory.Set(ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL, new DateTime(year, month, day, hour, minutes, seconds, DateTimeKind.Unspecified));
+        }
+        else
+        {
+            directory.addError("Error processing Reconyx UltraFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time.");
+        }*/
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagDayOfWeek, reader.GetByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagDayOfWeek));
+
+        directory.setInt(ReconyxUltraFireMakernoteDirectory.TAG_MOON_PHASE, reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_MOON_PHASE));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagAmbientTemperatureFahrenheit, ByteConvert.FromBigEndianToNative(reader.GetInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagAmbientTemperatureFahrenheit)));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagAmbientTemperature, ByteConvert.FromBigEndianToNative(reader.GetInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagAmbientTemperature)));
+
+        directory.setInt(ReconyxUltraFireMakernoteDirectory.TAG_FLASH, reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_FLASH));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagBatteryVoltage, ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagBatteryVoltage)) / 1000.0);
+        directory.setStringValue(ReconyxUltraFireMakernoteDirectory.TAG_SERIAL_NUMBER, new StringValue(reader.getBytes(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SERIAL_NUMBER, 14), Charsets.UTF_8));
+        // unread byte: the serial number's terminating null
+        directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_USER_LABEL, reader.getNullTerminatedString(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_USER_LABEL, 20, Charsets.UTF_8));
+    }
 }
 
Index: /trunk/src/com/drew/metadata/exif/GpsDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -36,4 +36,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class GpsDescriptor extends TagDescriptor<GpsDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/GpsDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -38,4 +38,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class GpsDirectory extends ExifDirectoryBase
 {
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java	(revision 13061)
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawDistortionDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawDistortionDirectory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawDistortionDescriptor extends TagDescriptor<PanasonicRawDistortionDirectory>
+{
+    public PanasonicRawDistortionDescriptor(@NotNull PanasonicRawDistortionDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagDistortionParam02:
+                return getDistortionParam02Description();
+            case TagDistortionParam04:
+                return getDistortionParam04Description();
+            case TagDistortionScale:
+                return getDistortionScaleDescription();
+            case TagDistortionCorrection:
+                return getDistortionCorrectionDescription();
+            case TagDistortionParam08:
+                return getDistortionParam08Description();
+            case TagDistortionParam09:
+                return getDistortionParam09Description();
+            case TagDistortionParam11:
+                return getDistortionParam11Description();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getWbTypeDescription(int tagType)
+    {
+        Integer wbtype = _directory.getInteger(tagType);
+        if (wbtype == null)
+            return null;
+
+        return super.getLightSourceDescription(wbtype.shortValue());
+    }
+
+    @Nullable
+    public String getDistortionParam02Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam02);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionParam04Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam04);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionScaleDescription()
+    {
+        Integer value = _directory.getInteger(TagDistortionScale);
+        if (value == null)
+            return null;
+
+        //return (1 / (1 + value / 32768)).toString();
+        return Integer.toString(1 / (1 + value / 32768));
+    }
+
+    @Nullable
+    public String getDistortionCorrectionDescription()
+    {
+        Integer value = _directory.getInteger(TagDistortionCorrection);
+        if (value == null)
+            return null;
+
+        // (have seen the upper 4 bits set for GF5 and GX1, giving a value of -4095 - PH)
+        int mask = 0x000f;
+        switch (value & mask)
+        {
+            case 0:
+                return "Off";
+            case 1:
+                return "On";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getDistortionParam08Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam08);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionParam09Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam09);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionParam11Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam11);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java	(revision 13061)
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawDistortionDirectory extends Directory
+{
+    // 0 and 1 are checksums
+
+    public static final int TagDistortionParam02 = 2;
+
+    public static final int TagDistortionParam04 = 4;
+    public static final int TagDistortionScale = 5;
+
+    public static final int TagDistortionCorrection = 7;
+    public static final int TagDistortionParam08 = 8;
+    public static final int TagDistortionParam09 = 9;
+
+    public static final int TagDistortionParam11 = 11;
+    public static final int TagDistortionN = 12;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagDistortionParam02, "Distortion Param 2");
+        _tagNameMap.put(TagDistortionParam04, "Distortion Param 4");
+        _tagNameMap.put(TagDistortionScale, "Distortion Scale");
+        _tagNameMap.put(TagDistortionCorrection, "Distortion Correction");
+        _tagNameMap.put(TagDistortionParam08, "Distortion Param 8");
+        _tagNameMap.put(TagDistortionParam09, "Distortion Param 9");
+        _tagNameMap.put(TagDistortionParam11, "Distortion Param 11");
+        _tagNameMap.put(TagDistortionN, "Distortion N");
+    }
+
+    public PanasonicRawDistortionDirectory()
+    {
+        this.setDescriptor(new PanasonicRawDistortionDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw DistortionInfo";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java	(revision 13061)
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawIFD0Directory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawIFD0Directory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawIFD0Descriptor extends TagDescriptor<PanasonicRawIFD0Directory>
+{
+    public PanasonicRawIFD0Descriptor(@NotNull PanasonicRawIFD0Directory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType)
+        {
+            case TagPanasonicRawVersion:
+                return getVersionBytesDescription(TagPanasonicRawVersion, 2);
+            case TagOrientation:
+                return getOrientationDescription(TagOrientation);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Directory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Directory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Directory.java	(revision 13061)
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags are found in IFD0 of Panasonic/Leica RAW, RW2 and RWL images.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawIFD0Directory extends Directory
+{
+    public static final int TagPanasonicRawVersion = 0x0001;
+    public static final int TagSensorWidth = 0x0002;
+    public static final int TagSensorHeight = 0x0003;
+    public static final int TagSensorTopBorder = 0x0004;
+    public static final int TagSensorLeftBorder = 0x0005;
+    public static final int TagSensorBottomBorder = 0x0006;
+    public static final int TagSensorRightBorder = 0x0007;
+
+    public static final int TagBlackLevel1 = 0x0008;
+    public static final int TagBlackLevel2 = 0x0009;
+    public static final int TagBlackLevel3 = 0x000a;
+    public static final int TagLinearityLimitRed = 0x000e;
+    public static final int TagLinearityLimitGreen = 0x000f;
+    public static final int TagLinearityLimitBlue = 0x0010;
+    public static final int TagRedBalance = 0x0011;
+    public static final int TagBlueBalance = 0x0012;
+    public static final int TagWbInfo = 0x0013;
+
+    public static final int TagIso = 0x0017;
+    public static final int TagHighIsoMultiplierRed = 0x0018;
+    public static final int TagHighIsoMultiplierGreen = 0x0019;
+    public static final int TagHighIsoMultiplierBlue = 0x001a;
+    public static final int TagBlackLevelRed = 0x001c;
+    public static final int TagBlackLevelGreen = 0x001d;
+    public static final int TagBlackLevelBlue = 0x001e;
+    public static final int TagWbRedLevel = 0x0024;
+    public static final int TagWbGreenLevel = 0x0025;
+    public static final int TagWbBlueLevel = 0x0026;
+
+    public static final int TagWbInfo2 = 0x0027;
+
+    public static final int TagJpgFromRaw = 0x002e;
+
+    public static final int TagCropTop = 0x002f;
+    public static final int TagCropLeft = 0x0030;
+    public static final int TagCropBottom = 0x0031;
+    public static final int TagCropRight = 0x0032;
+
+    public static final int TagMake = 0x010f;
+    public static final int TagModel = 0x0110;
+    public static final int TagStripOffsets = 0x0111;
+    public static final int TagOrientation = 0x0112;
+    public static final int TagRowsPerStrip = 0x0116;
+    public static final int TagStripByteCounts = 0x0117;
+    public static final int TagRawDataOffset = 0x0118;
+
+    public static final int TagDistortionInfo = 0x0119;
+
+    public PanasonicRawIFD0Directory()
+    {
+        this.setDescriptor(new PanasonicRawIFD0Descriptor(this));
+    }
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagPanasonicRawVersion, "Panasonic Raw Version");
+        _tagNameMap.put(TagSensorWidth, "Sensor Width");
+        _tagNameMap.put(TagSensorHeight, "Sensor Height");
+        _tagNameMap.put(TagSensorTopBorder, "Sensor Top Border");
+        _tagNameMap.put(TagSensorLeftBorder, "Sensor Left Border");
+        _tagNameMap.put(TagSensorBottomBorder, "Sensor Bottom Border");
+        _tagNameMap.put(TagSensorRightBorder, "Sensor Right Border");
+
+        _tagNameMap.put(TagBlackLevel1, "Black Level 1");
+        _tagNameMap.put(TagBlackLevel2, "Black Level 2");
+        _tagNameMap.put(TagBlackLevel3, "Black Level 3");
+        _tagNameMap.put(TagLinearityLimitRed, "Linearity Limit Red");
+        _tagNameMap.put(TagLinearityLimitGreen, "Linearity Limit Green");
+        _tagNameMap.put(TagLinearityLimitBlue, "Linearity Limit Blue");
+        _tagNameMap.put(TagRedBalance, "Red Balance");
+        _tagNameMap.put(TagBlueBalance, "Blue Balance");
+
+        _tagNameMap.put(TagIso, "ISO");
+        _tagNameMap.put(TagHighIsoMultiplierRed, "High ISO Multiplier Red");
+        _tagNameMap.put(TagHighIsoMultiplierGreen, "High ISO Multiplier Green");
+        _tagNameMap.put(TagHighIsoMultiplierBlue, "High ISO Multiplier Blue");
+        _tagNameMap.put(TagBlackLevelRed, "Black Level Red");
+        _tagNameMap.put(TagBlackLevelGreen, "Black Level Green");
+        _tagNameMap.put(TagBlackLevelBlue, "Black Level Blue");
+        _tagNameMap.put(TagWbRedLevel, "WB Red Level");
+        _tagNameMap.put(TagWbGreenLevel, "WB Green Level");
+        _tagNameMap.put(TagWbBlueLevel, "WB Blue Level");
+
+        _tagNameMap.put(TagJpgFromRaw, "Jpg From Raw");
+
+        _tagNameMap.put(TagCropTop, "Crop Top");
+        _tagNameMap.put(TagCropLeft, "Crop Left");
+        _tagNameMap.put(TagCropBottom, "Crop Bottom");
+        _tagNameMap.put(TagCropRight, "Crop Right");
+
+        _tagNameMap.put(TagMake, "Make");
+        _tagNameMap.put(TagModel, "Model");
+        _tagNameMap.put(TagStripOffsets, "Strip Offsets");
+        _tagNameMap.put(TagOrientation, "Orientation");
+        _tagNameMap.put(TagRowsPerStrip, "Rows Per Strip");
+        _tagNameMap.put(TagStripByteCounts, "Strip Byte Counts");
+        _tagNameMap.put(TagRawDataOffset, "Raw Data Offset");
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw Exif IFD0";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java	(revision 13061)
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawWbInfo2Directory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawWbInfo2Directory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfo2Descriptor extends TagDescriptor<PanasonicRawWbInfo2Directory>
+{
+    public PanasonicRawWbInfo2Descriptor(@NotNull PanasonicRawWbInfo2Directory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagWbType1:
+            case TagWbType2:
+            case TagWbType3:
+            case TagWbType4:
+            case TagWbType5:
+            case TagWbType6:
+            case TagWbType7:
+                return getWbTypeDescription(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getWbTypeDescription(int tagType)
+    {
+        Integer wbtype = _directory.getInteger(tagType);
+        if (wbtype == null)
+            return null;
+
+        return super.getLightSourceDescription(wbtype.shortValue());
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java	(revision 13061)
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfo2Directory extends Directory
+{
+    public static final int TagNumWbEntries = 0;
+
+    public static final int TagWbType1 = 1;
+    public static final int TagWbRgbLevels1 = 2;
+
+    public static final int TagWbType2 = 5;
+    public static final int TagWbRgbLevels2 = 6;
+
+    public static final int TagWbType3 = 9;
+    public static final int TagWbRgbLevels3 = 10;
+
+    public static final int TagWbType4 = 13;
+    public static final int TagWbRgbLevels4 = 14;
+
+    public static final int TagWbType5 = 17;
+    public static final int TagWbRgbLevels5 = 18;
+
+    public static final int TagWbType6 = 21;
+    public static final int TagWbRgbLevels6 = 22;
+
+    public static final int TagWbType7 = 25;
+    public static final int TagWbRgbLevels7 = 26;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagNumWbEntries, "Num WB Entries");
+        _tagNameMap.put(TagNumWbEntries, "Num WB Entries");
+        _tagNameMap.put(TagWbType1, "WB Type 1");
+        _tagNameMap.put(TagWbRgbLevels1, "WB RGB Levels 1");
+        _tagNameMap.put(TagWbType2, "WB Type 2");
+        _tagNameMap.put(TagWbRgbLevels2, "WB RGB Levels 2");
+        _tagNameMap.put(TagWbType3, "WB Type 3");
+        _tagNameMap.put(TagWbRgbLevels3, "WB RGB Levels 3");
+        _tagNameMap.put(TagWbType4, "WB Type 4");
+        _tagNameMap.put(TagWbRgbLevels4, "WB RGB Levels 4");
+        _tagNameMap.put(TagWbType5, "WB Type 5");
+        _tagNameMap.put(TagWbRgbLevels5, "WB RGB Levels 5");
+        _tagNameMap.put(TagWbType6, "WB Type 6");
+        _tagNameMap.put(TagWbRgbLevels6, "WB RGB Levels 6");
+        _tagNameMap.put(TagWbType7, "WB Type 7");
+        _tagNameMap.put(TagWbRgbLevels7, "WB RGB Levels 7");
+    }
+
+    public PanasonicRawWbInfo2Directory()
+    {
+        this.setDescriptor(new PanasonicRawWbInfo2Descriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw WbInfo2";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java	(revision 13061)
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawWbInfoDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawWbInfoDirectory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfoDescriptor extends TagDescriptor<PanasonicRawWbInfoDirectory>
+{
+    public PanasonicRawWbInfoDescriptor(@NotNull PanasonicRawWbInfoDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagWbType1:
+            case TagWbType2:
+            case TagWbType3:
+            case TagWbType4:
+            case TagWbType5:
+            case TagWbType6:
+            case TagWbType7:
+                return getWbTypeDescription(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getWbTypeDescription(int tagType)
+    {
+        Integer wbtype = _directory.getInteger(tagType);
+        if (wbtype == null)
+            return null;
+
+        return super.getLightSourceDescription(wbtype.shortValue());
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java	(revision 13061)
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfoDirectory extends Directory
+{
+    public static final int TagNumWbEntries = 0;
+
+    public static final int TagWbType1 = 1;
+    public static final int TagWbRbLevels1 = 2;
+
+    public static final int TagWbType2 = 4;
+    public static final int TagWbRbLevels2 = 5;
+
+    public static final int TagWbType3 = 7;
+    public static final int TagWbRbLevels3 = 8;
+
+    public static final int TagWbType4 = 10;
+    public static final int TagWbRbLevels4 = 11;
+
+    public static final int TagWbType5 = 13;
+    public static final int TagWbRbLevels5 = 14;
+
+    public static final int TagWbType6 = 16;
+    public static final int TagWbRbLevels6 = 17;
+
+    public static final int TagWbType7 = 19;
+    public static final int TagWbRbLevels7 = 20;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagNumWbEntries, "Num WB Entries");
+        _tagNameMap.put(TagWbType1, "WB Type 1");
+        _tagNameMap.put(TagWbRbLevels1, "WB RGB Levels 1");
+        _tagNameMap.put(TagWbType2, "WB Type 2");
+        _tagNameMap.put(TagWbRbLevels2, "WB RGB Levels 2");
+        _tagNameMap.put(TagWbType3, "WB Type 3");
+        _tagNameMap.put(TagWbRbLevels3, "WB RGB Levels 3");
+        _tagNameMap.put(TagWbType4, "WB Type 4");
+        _tagNameMap.put(TagWbRbLevels4, "WB RGB Levels 4");
+        _tagNameMap.put(TagWbType5, "WB Type 5");
+        _tagNameMap.put(TagWbRbLevels5, "WB RGB Levels 5");
+        _tagNameMap.put(TagWbType6, "WB Type 6");
+        _tagNameMap.put(TagWbRbLevels6, "WB RGB Levels 6");
+        _tagNameMap.put(TagWbType7, "WB Type 7");
+        _tagNameMap.put(TagWbRbLevels7, "WB RGB Levels 7");
+    }
+
+    public PanasonicRawWbInfoDirectory()
+    {
+        this.setDescriptor(new PanasonicRawWbInfoDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw WbInfo";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PrintIMDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PrintIMDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PrintIMDescriptor.java	(revision 13061)
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PrintIMDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PrintIMDirectory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PrintIMDescriptor extends TagDescriptor<PrintIMDirectory>
+{
+    public PrintIMDescriptor(@NotNull PrintIMDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagPrintImVersion:
+                return super.getDescription(tagType);
+            default:
+                Integer value = _directory.getInteger(tagType);
+                if (value == null)
+                    return null;
+                return String.format("0x%08x", value);
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/PrintIMDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/PrintIMDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/PrintIMDirectory.java	(revision 13061)
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Epson proprietary metadata. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PrintIMDirectory extends Directory
+{
+    public static final int TagPrintImVersion = 0x0000;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagPrintImVersion, "PrintIM Version");
+    }
+
+    public PrintIMDirectory()
+    {
+        this.setDescriptor(new PrintIMDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PrintIM";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link AppleMakernoteDirectory}.
+ * <p>
+ * Using information from http://owl.phy.queensu.ca/~phil/exiftool/TagNames/Apple.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class AppleMakernoteDescriptor extends TagDescriptor<AppleMakernoteDirectory>
+{
+    public AppleMakernoteDescriptor(@NotNull AppleMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case AppleMakernoteDirectory.TAG_HDR_IMAGE_TYPE:
+                return getHdrImageTypeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getHdrImageTypeDescription()
+    {
+        return getIndexedDescription(AppleMakernoteDirectory.TAG_HDR_IMAGE_TYPE, 3, "HDR Image", "Original Image");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Apple cameras.
+ * <p>
+ * Using information from http://owl.phy.queensu.ca/~phil/exiftool/TagNames/Apple.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class AppleMakernoteDirectory extends Directory
+{
+    public static final int TAG_RUN_TIME = 0x0003;
+    public static final int TAG_HDR_IMAGE_TYPE = 0x000a;
+    public static final int TAG_BURST_UUID = 0x000b;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_RUN_TIME, "Run Time");
+        _tagNameMap.put(TAG_HDR_IMAGE_TYPE, "HDR Image Type");
+        _tagNameMap.put(TAG_BURST_UUID, "Burst UUID");
+    }
+
+    public AppleMakernoteDirectory()
+    {
+        this.setDescriptor(new AppleMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Apple Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,4 +26,5 @@
 
 import java.text.DecimalFormat;
+import java.util.HashMap;
 
 import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
@@ -34,4 +35,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory>
 {
@@ -54,4 +56,6 @@
             case CameraSettings.TAG_DIGITAL_ZOOM:
                 return getDigitalZoomDescription();
+            case CameraSettings.TAG_RECORD_MODE:
+                return getRecordModeDescription();
             case CameraSettings.TAG_QUALITY:
                 return getQualityDescription();
@@ -102,4 +106,26 @@
             case FocalLength.TAG_FLASH_BIAS:
                 return getFlashBiasDescription();
+            case AFInfo.TAG_AF_POINTS_IN_FOCUS:
+                return getTagAfPointsInFocus();
+            case CameraSettings.TAG_MAX_APERTURE:
+                return getMaxApertureDescription();
+            case CameraSettings.TAG_MIN_APERTURE:
+                return getMinApertureDescription();
+            case CameraSettings.TAG_FOCUS_CONTINUOUS:
+                return getFocusContinuousDescription();
+            case CameraSettings.TAG_AE_SETTING:
+                return getAESettingDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_DISPLAY_APERTURE:
+                return getDisplayApertureDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_SPOT_METERING_MODE:
+                return getSpotMeteringModeDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_PHOTO_EFFECT:
+                return getPhotoEffectDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_MANUAL_FLASH_OUTPUT:
+                return getManualFlashOutputDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_COLOR_TONE:
+                return getColorToneDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY:
+                return getSRawQualityDescription();
 
             // It turns out that these values are dependent upon the camera model and therefore the below code was
@@ -346,5 +372,5 @@
         //  0, 0.33,  0.5, 0.66,  1
 
-        return ((isNegative) ? "-" : "") + Float.toString(value / 32f) + " EV";
+        return (isNegative ? "-" : "") + Float.toString(value / 32f) + " EV";
     }
 
@@ -364,4 +390,26 @@
             return "Unknown (" + value + ")";
         }
+    }
+
+    @Nullable
+    public String getTagAfPointsInFocus()
+    {
+        Integer value = _directory.getInteger(AFInfo.TAG_AF_POINTS_IN_FOCUS);
+        if (value == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        for (int i = 0; i < 16; i++)
+        {
+            if ((value & 1 << i) != 0)
+            {
+                if (sb.length() != 0)
+                    sb.append(',');
+                sb.append(i);
+            }
+        }
+
+        return sb.length() == 0 ? "None" : sb.toString();
     }
 
@@ -393,14 +441,14 @@
         if (value == null)
             return null;
-        if (((value >> 14) & 1) > 0) {
+        if (((value >> 14) & 1) != 0) {
             return "External E-TTL";
         }
-        if (((value >> 13) & 1) > 0) {
+        if (((value >> 13) & 1) != 0) {
             return "Internal flash";
         }
-        if (((value >> 11) & 1) > 0) {
+        if (((value >> 11) & 1) != 0) {
             return "FP sync used";
         }
-        if (((value >> 4) & 1) > 0) {
+        if (((value >> 4) & 1) != 0) {
             return "FP sync enabled";
         }
@@ -461,5 +509,29 @@
             return null;
 
-        return "Lens type: " + Integer.toString(value);
+        return _lensTypeById.containsKey(value)
+            ? _lensTypeById.get(value)
+            : String.format("Unknown (%d)", value);
+    }
+
+    @Nullable
+    public String getMaxApertureDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_MAX_APERTURE);
+        if (value == null)
+            return null;
+        if (value > 512)
+            return String.format("Unknown (%d)", value);
+        return getFStopDescription(Math.exp(decodeCanonEv(value) * Math.log(2.0) / 2.0));
+    }
+
+    @Nullable
+    public String getMinApertureDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_MIN_APERTURE);
+        if (value == null)
+            return null;
+        if (value > 512)
+            return String.format("Unknown (%d)", value);
+        return getFStopDescription(Math.exp(decodeCanonEv(value) * Math.log(2.0) / 2.0));
     }
 
@@ -499,5 +571,5 @@
         // Canon PowerShot S3 is special
         int canonMask = 0x4000;
-        if ((value & canonMask) > 0)
+        if ((value & canonMask) != 0)
             return "" + (value & ~canonMask);
 
@@ -700,4 +772,10 @@
 
     @Nullable
+    public String getRecordModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_RECORD_MODE, 1, "JPEG", "CRW+THM", "AVI+THM", "TIF", "TIF+JPEG", "CR2", "CR2+JPEG", null, "MOV", "MP4");
+    }
+
+    @Nullable
     public String getFocusTypeDescription()
     {
@@ -724,3 +802,354 @@
         return getIndexedDescription(CameraSettings.TAG_FLASH_ACTIVITY, "Flash did not fire", "Flash fired");
     }
+
+    @Nullable
+    public String getFocusContinuousDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_CONTINUOUS, 0,
+            "Single", "Continuous", null, null, null, null, null, null, "Manual");
+    }
+
+    @Nullable
+    public String getAESettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_AE_SETTING, 0,
+            "Normal AE", "Exposure Compensation", "AE Lock", "AE Lock + Exposure Comp.", "No AE");
+    }
+
+    @Nullable
+    public String getDisplayApertureDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_DISPLAY_APERTURE);
+        if (value == null)
+            return null;
+
+        if (value == 0xFFFF)
+            return value.toString();
+        return getFStopDescription(value / 10f);
+    }
+
+    @Nullable
+    public String getSpotMeteringModeDescription()
+    {
+        return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SPOT_METERING_MODE, 0,
+            "Center", "AF Point");
+    }
+
+    @Nullable
+    public String getPhotoEffectDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_PHOTO_EFFECT);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "Off";
+            case 1:
+                return "Vivid";
+            case 2:
+                return "Neutral";
+            case 3:
+                return "Smooth";
+            case 4:
+                return "Sepia";
+            case 5:
+                return "B&W";
+            case 6:
+                return "Custom";
+            case 100:
+                return "My Color Data";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getManualFlashOutputDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_MANUAL_FLASH_OUTPUT);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "n/a";
+            case 0x500:
+                return "Full";
+            case 0x502:
+                return "Medium";
+            case 0x504:
+                return "Low";
+            case 0x7fff:
+                return "n/a";   // (EOS models)
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorToneDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_COLOR_TONE);
+        if (value == null)
+            return null;
+
+        return value == 0x7fff ? "n/a" : value.toString();
+    }
+
+    @Nullable
+    public String getSRawQualityDescription()
+    {
+        return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY, 0, "n/a", "sRAW1 (mRAW)", "sRAW2 (sRAW)");
+    }
+
+    /**
+     * Canon hex-based EV (modulo 0x20) to real number.
+     *
+     * Converted from Exiftool version 10.10 created by Phil Harvey
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/
+     * lib\Image\ExifTool\Canon.pm
+     *
+     *         eg) 0x00 -> 0
+     *             0x0c -> 0.33333
+     *             0x10 -> 0.5
+     *             0x14 -> 0.66666
+     *             0x20 -> 1   ... etc
+     */
+    private double decodeCanonEv(int val)
+    {
+        int sign = 1;
+        if (val < 0)
+        {
+            val = -val;
+            sign = -1;
+        }
+
+        int frac = val & 0x1f;
+        val -= frac;
+
+        if (frac == 0x0c)
+            frac = 0x20 / 3;
+        else if (frac == 0x14)
+            frac = 0x40 / 3;
+
+        return sign * (val + frac) / (double)0x20;
+    }
+
+    /**
+     *  Map from <see cref="CanonMakernoteDirectory.CameraSettings.TagLensType"/> to string descriptions.
+     *
+     *  Data sourced from http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html#LensType
+     *
+     *  Note that only Canon lenses are listed. Lenses from other manufacturers may identify themselves to the camera
+     *  as being from this set, but in fact may be quite different. This limits the usefulness of this data,
+     *  unfortunately.
+     */
+    private static final HashMap<Integer, String> _lensTypeById = new HashMap<Integer, String>();
+
+    static {
+        _lensTypeById.put(1, "Canon EF 50mm f/1.8");
+        _lensTypeById.put(2, "Canon EF 28mm f/2.8");
+        _lensTypeById.put(3, "Canon EF 135mm f/2.8 Soft");
+        _lensTypeById.put(4, "Canon EF 35-105mm f/3.5-4.5 or Sigma Lens");
+        _lensTypeById.put(5, "Canon EF 35-70mm f/3.5-4.5");
+        _lensTypeById.put(6, "Canon EF 28-70mm f/3.5-4.5 or Sigma or Tokina Lens");
+        _lensTypeById.put(7, "Canon EF 100-300mm f/5.6L");
+        _lensTypeById.put(8, "Canon EF 100-300mm f/5.6 or Sigma or Tokina Lens");
+        _lensTypeById.put(9, "Canon EF 70-210mm f/4");
+        _lensTypeById.put(10, "Canon EF 50mm f/2.5 Macro or Sigma Lens");
+        _lensTypeById.put(11, "Canon EF 35mm f/2");
+        _lensTypeById.put(13, "Canon EF 15mm f/2.8 Fisheye");
+        _lensTypeById.put(14, "Canon EF 50-200mm f/3.5-4.5L");
+        _lensTypeById.put(15, "Canon EF 50-200mm f/3.5-4.5");
+        _lensTypeById.put(16, "Canon EF 35-135mm f/3.5-4.5");
+        _lensTypeById.put(17, "Canon EF 35-70mm f/3.5-4.5A");
+        _lensTypeById.put(18, "Canon EF 28-70mm f/3.5-4.5");
+        _lensTypeById.put(20, "Canon EF 100-200mm f/4.5A");
+        _lensTypeById.put(21, "Canon EF 80-200mm f/2.8L");
+        _lensTypeById.put(22, "Canon EF 20-35mm f/2.8L or Tokina Lens");
+        _lensTypeById.put(23, "Canon EF 35-105mm f/3.5-4.5");
+        _lensTypeById.put(24, "Canon EF 35-80mm f/4-5.6 Power Zoom");
+        _lensTypeById.put(25, "Canon EF 35-80mm f/4-5.6 Power Zoom");
+        _lensTypeById.put(26, "Canon EF 100mm f/2.8 Macro or Other Lens");
+        _lensTypeById.put(27, "Canon EF 35-80mm f/4-5.6");
+        _lensTypeById.put(28, "Canon EF 80-200mm f/4.5-5.6 or Tamron Lens");
+        _lensTypeById.put(29, "Canon EF 50mm f/1.8 II");
+        _lensTypeById.put(30, "Canon EF 35-105mm f/4.5-5.6");
+        _lensTypeById.put(31, "Canon EF 75-300mm f/4-5.6 or Tamron Lens");
+        _lensTypeById.put(32, "Canon EF 24mm f/2.8 or Sigma Lens");
+        _lensTypeById.put(33, "Voigtlander or Carl Zeiss Lens");
+        _lensTypeById.put(35, "Canon EF 35-80mm f/4-5.6");
+        _lensTypeById.put(36, "Canon EF 38-76mm f/4.5-5.6");
+        _lensTypeById.put(37, "Canon EF 35-80mm f/4-5.6 or Tamron Lens");
+        _lensTypeById.put(38, "Canon EF 80-200mm f/4.5-5.6");
+        _lensTypeById.put(39, "Canon EF 75-300mm f/4-5.6");
+        _lensTypeById.put(40, "Canon EF 28-80mm f/3.5-5.6");
+        _lensTypeById.put(41, "Canon EF 28-90mm f/4-5.6");
+        _lensTypeById.put(42, "Canon EF 28-200mm f/3.5-5.6 or Tamron Lens");
+        _lensTypeById.put(43, "Canon EF 28-105mm f/4-5.6");
+        _lensTypeById.put(44, "Canon EF 90-300mm f/4.5-5.6");
+        _lensTypeById.put(45, "Canon EF-S 18-55mm f/3.5-5.6 [II]");
+        _lensTypeById.put(46, "Canon EF 28-90mm f/4-5.6");
+        _lensTypeById.put(47, "Zeiss Milvus 35mm f/2 or 50mm f/2");
+        _lensTypeById.put(48, "Canon EF-S 18-55mm f/3.5-5.6 IS");
+        _lensTypeById.put(49, "Canon EF-S 55-250mm f/4-5.6 IS");
+        _lensTypeById.put(50, "Canon EF-S 18-200mm f/3.5-5.6 IS");
+        _lensTypeById.put(51, "Canon EF-S 18-135mm f/3.5-5.6 IS");
+        _lensTypeById.put(52, "Canon EF-S 18-55mm f/3.5-5.6 IS II");
+        _lensTypeById.put(53, "Canon EF-S 18-55mm f/3.5-5.6 III");
+        _lensTypeById.put(54, "Canon EF-S 55-250mm f/4-5.6 IS II");
+        _lensTypeById.put(94, "Canon TS-E 17mm f/4L");
+        _lensTypeById.put(95, "Canon TS-E 24.0mm f/3.5 L II");
+        _lensTypeById.put(124, "Canon MP-E 65mm f/2.8 1-5x Macro Photo");
+        _lensTypeById.put(125, "Canon TS-E 24mm f/3.5L");
+        _lensTypeById.put(126, "Canon TS-E 45mm f/2.8");
+        _lensTypeById.put(127, "Canon TS-E 90mm f/2.8");
+        _lensTypeById.put(129, "Canon EF 300mm f/2.8L");
+        _lensTypeById.put(130, "Canon EF 50mm f/1.0L");
+        _lensTypeById.put(131, "Canon EF 28-80mm f/2.8-4L or Sigma Lens");
+        _lensTypeById.put(132, "Canon EF 1200mm f/5.6L");
+        _lensTypeById.put(134, "Canon EF 600mm f/4L IS");
+        _lensTypeById.put(135, "Canon EF 200mm f/1.8L");
+        _lensTypeById.put(136, "Canon EF 300mm f/2.8L");
+        _lensTypeById.put(137, "Canon EF 85mm f/1.2L or Sigma or Tamron Lens");
+        _lensTypeById.put(138, "Canon EF 28-80mm f/2.8-4L");
+        _lensTypeById.put(139, "Canon EF 400mm f/2.8L");
+        _lensTypeById.put(140, "Canon EF 500mm f/4.5L");
+        _lensTypeById.put(141, "Canon EF 500mm f/4.5L");
+        _lensTypeById.put(142, "Canon EF 300mm f/2.8L IS");
+        _lensTypeById.put(143, "Canon EF 500mm f/4L IS or Sigma Lens");
+        _lensTypeById.put(144, "Canon EF 35-135mm f/4-5.6 USM");
+        _lensTypeById.put(145, "Canon EF 100-300mm f/4.5-5.6 USM");
+        _lensTypeById.put(146, "Canon EF 70-210mm f/3.5-4.5 USM");
+        _lensTypeById.put(147, "Canon EF 35-135mm f/4-5.6 USM");
+        _lensTypeById.put(148, "Canon EF 28-80mm f/3.5-5.6 USM");
+        _lensTypeById.put(149, "Canon EF 100mm f/2 USM");
+        _lensTypeById.put(150, "Canon EF 14mm f/2.8L or Sigma Lens");
+        _lensTypeById.put(151, "Canon EF 200mm f/2.8L");
+        _lensTypeById.put(152, "Canon EF 300mm f/4L IS or Sigma Lens");
+        _lensTypeById.put(153, "Canon EF 35-350mm f/3.5-5.6L or Sigma or Tamron Lens");
+        _lensTypeById.put(154, "Canon EF 20mm f/2.8 USM or Zeiss Lens");
+        _lensTypeById.put(155, "Canon EF 85mm f/1.8 USM");
+        _lensTypeById.put(156, "Canon EF 28-105mm f/3.5-4.5 USM or Tamron Lens");
+        _lensTypeById.put(160, "Canon EF 20-35mm f/3.5-4.5 USM or Tamron or Tokina Lens");
+        _lensTypeById.put(161, "Canon EF 28-70mm f/2.8L or Sigma or Tamron Lens");
+        _lensTypeById.put(162, "Canon EF 200mm f/2.8L");
+        _lensTypeById.put(163, "Canon EF 300mm f/4L");
+        _lensTypeById.put(164, "Canon EF 400mm f/5.6L");
+        _lensTypeById.put(165, "Canon EF 70-200mm f/2.8 L");
+        _lensTypeById.put(166, "Canon EF 70-200mm f/2.8 L + 1.4x");
+        _lensTypeById.put(167, "Canon EF 70-200mm f/2.8 L + 2x");
+        _lensTypeById.put(168, "Canon EF 28mm f/1.8 USM or Sigma Lens");
+        _lensTypeById.put(169, "Canon EF 17-35mm f/2.8L or Sigma Lens");
+        _lensTypeById.put(170, "Canon EF 200mm f/2.8L II");
+        _lensTypeById.put(171, "Canon EF 300mm f/4L");
+        _lensTypeById.put(172, "Canon EF 400mm f/5.6L or Sigma Lens");
+        _lensTypeById.put(173, "Canon EF 180mm Macro f/3.5L or Sigma Lens");
+        _lensTypeById.put(174, "Canon EF 135mm f/2L or Other Lens");
+        _lensTypeById.put(175, "Canon EF 400mm f/2.8L");
+        _lensTypeById.put(176, "Canon EF 24-85mm f/3.5-4.5 USM");
+        _lensTypeById.put(177, "Canon EF 300mm f/4L IS");
+        _lensTypeById.put(178, "Canon EF 28-135mm f/3.5-5.6 IS");
+        _lensTypeById.put(179, "Canon EF 24mm f/1.4L");
+        _lensTypeById.put(180, "Canon EF 35mm f/1.4L or Other Lens");
+        _lensTypeById.put(181, "Canon EF 100-400mm f/4.5-5.6L IS + 1.4x or Sigma Lens");
+        _lensTypeById.put(182, "Canon EF 100-400mm f/4.5-5.6L IS + 2x or Sigma Lens");
+        _lensTypeById.put(183, "Canon EF 100-400mm f/4.5-5.6L IS or Sigma Lens");
+        _lensTypeById.put(184, "Canon EF 400mm f/2.8L + 2x");
+        _lensTypeById.put(185, "Canon EF 600mm f/4L IS");
+        _lensTypeById.put(186, "Canon EF 70-200mm f/4L");
+        _lensTypeById.put(187, "Canon EF 70-200mm f/4L + 1.4x");
+        _lensTypeById.put(188, "Canon EF 70-200mm f/4L + 2x");
+        _lensTypeById.put(189, "Canon EF 70-200mm f/4L + 2.8x");
+        _lensTypeById.put(190, "Canon EF 100mm f/2.8 Macro USM");
+        _lensTypeById.put(191, "Canon EF 400mm f/4 DO IS");
+        _lensTypeById.put(193, "Canon EF 35-80mm f/4-5.6 USM");
+        _lensTypeById.put(194, "Canon EF 80-200mm f/4.5-5.6 USM");
+        _lensTypeById.put(195, "Canon EF 35-105mm f/4.5-5.6 USM");
+        _lensTypeById.put(196, "Canon EF 75-300mm f/4-5.6 USM");
+        _lensTypeById.put(197, "Canon EF 75-300mm f/4-5.6 IS USM");
+        _lensTypeById.put(198, "Canon EF 50mm f/1.4 USM or Zeiss Lens");
+        _lensTypeById.put(199, "Canon EF 28-80mm f/3.5-5.6 USM");
+        _lensTypeById.put(200, "Canon EF 75-300mm f/4-5.6 USM");
+        _lensTypeById.put(201, "Canon EF 28-80mm f/3.5-5.6 USM");
+        _lensTypeById.put(202, "Canon EF 28-80mm f/3.5-5.6 USM IV");
+        _lensTypeById.put(208, "Canon EF 22-55mm f/4-5.6 USM");
+        _lensTypeById.put(209, "Canon EF 55-200mm f/4.5-5.6");
+        _lensTypeById.put(210, "Canon EF 28-90mm f/4-5.6 USM");
+        _lensTypeById.put(211, "Canon EF 28-200mm f/3.5-5.6 USM");
+        _lensTypeById.put(212, "Canon EF 28-105mm f/4-5.6 USM");
+        _lensTypeById.put(213, "Canon EF 90-300mm f/4.5-5.6 USM or Tamron Lens");
+        _lensTypeById.put(214, "Canon EF-S 18-55mm f/3.5-5.6 USM");
+        _lensTypeById.put(215, "Canon EF 55-200mm f/4.5-5.6 II USM");
+        _lensTypeById.put(217, "Tamron AF 18-270mm f/3.5-6.3 Di II VC PZD");
+        _lensTypeById.put(224, "Canon EF 70-200mm f/2.8L IS");
+        _lensTypeById.put(225, "Canon EF 70-200mm f/2.8L IS + 1.4x");
+        _lensTypeById.put(226, "Canon EF 70-200mm f/2.8L IS + 2x");
+        _lensTypeById.put(227, "Canon EF 70-200mm f/2.8L IS + 2.8x");
+        _lensTypeById.put(228, "Canon EF 28-105mm f/3.5-4.5 USM");
+        _lensTypeById.put(229, "Canon EF 16-35mm f/2.8L");
+        _lensTypeById.put(230, "Canon EF 24-70mm f/2.8L");
+        _lensTypeById.put(231, "Canon EF 17-40mm f/4L");
+        _lensTypeById.put(232, "Canon EF 70-300mm f/4.5-5.6 DO IS USM");
+        _lensTypeById.put(233, "Canon EF 28-300mm f/3.5-5.6L IS");
+        _lensTypeById.put(234, "Canon EF-S 17-85mm f/4-5.6 IS USM or Tokina Lens");
+        _lensTypeById.put(235, "Canon EF-S 10-22mm f/3.5-4.5 USM");
+        _lensTypeById.put(236, "Canon EF-S 60mm f/2.8 Macro USM");
+        _lensTypeById.put(237, "Canon EF 24-105mm f/4L IS");
+        _lensTypeById.put(238, "Canon EF 70-300mm f/4-5.6 IS USM");
+        _lensTypeById.put(239, "Canon EF 85mm f/1.2L II");
+        _lensTypeById.put(240, "Canon EF-S 17-55mm f/2.8 IS USM");
+        _lensTypeById.put(241, "Canon EF 50mm f/1.2L");
+        _lensTypeById.put(242, "Canon EF 70-200mm f/4L IS");
+        _lensTypeById.put(243, "Canon EF 70-200mm f/4L IS + 1.4x");
+        _lensTypeById.put(244, "Canon EF 70-200mm f/4L IS + 2x");
+        _lensTypeById.put(245, "Canon EF 70-200mm f/4L IS + 2.8x");
+        _lensTypeById.put(246, "Canon EF 16-35mm f/2.8L II");
+        _lensTypeById.put(247, "Canon EF 14mm f/2.8L II USM");
+        _lensTypeById.put(248, "Canon EF 200mm f/2L IS or Sigma Lens");
+        _lensTypeById.put(249, "Canon EF 800mm f/5.6L IS");
+        _lensTypeById.put(250, "Canon EF 24mm f/1.4L II or Sigma Lens");
+        _lensTypeById.put(251, "Canon EF 70-200mm f/2.8L IS II USM");
+        _lensTypeById.put(252, "Canon EF 70-200mm f/2.8L IS II USM + 1.4x");
+        _lensTypeById.put(253, "Canon EF 70-200mm f/2.8L IS II USM + 2x");
+        _lensTypeById.put(254, "Canon EF 100mm f/2.8L Macro IS USM");
+        _lensTypeById.put(255, "Sigma 24-105mm f/4 DG OS HSM | A or Other Sigma Lens");
+        _lensTypeById.put(488, "Canon EF-S 15-85mm f/3.5-5.6 IS USM");
+        _lensTypeById.put(489, "Canon EF 70-300mm f/4-5.6L IS USM");
+        _lensTypeById.put(490, "Canon EF 8-15mm f/4L Fisheye USM");
+        _lensTypeById.put(491, "Canon EF 300mm f/2.8L IS II USM");
+        _lensTypeById.put(492, "Canon EF 400mm f/2.8L IS II USM");
+        _lensTypeById.put(493, "Canon EF 500mm f/4L IS II USM or EF 24-105mm f4L IS USM");
+        _lensTypeById.put(494, "Canon EF 600mm f/4.0L IS II USM");
+        _lensTypeById.put(495, "Canon EF 24-70mm f/2.8L II USM");
+        _lensTypeById.put(496, "Canon EF 200-400mm f/4L IS USM");
+        _lensTypeById.put(499, "Canon EF 200-400mm f/4L IS USM + 1.4x");
+        _lensTypeById.put(502, "Canon EF 28mm f/2.8 IS USM");
+        _lensTypeById.put(503, "Canon EF 24mm f/2.8 IS USM");
+        _lensTypeById.put(504, "Canon EF 24-70mm f/4L IS USM");
+        _lensTypeById.put(505, "Canon EF 35mm f/2 IS USM");
+        _lensTypeById.put(506, "Canon EF 400mm f/4 DO IS II USM");
+        _lensTypeById.put(507, "Canon EF 16-35mm f/4L IS USM");
+        _lensTypeById.put(508, "Canon EF 11-24mm f/4L USM");
+        _lensTypeById.put(747, "Canon EF 100-400mm f/4.5-5.6L IS II USM");
+        _lensTypeById.put(750, "Canon EF 35mm f/1.4L II USM");
+        _lensTypeById.put(4142, "Canon EF-S 18-135mm f/3.5-5.6 IS STM");
+        _lensTypeById.put(4143, "Canon EF-M 18-55mm f/3.5-5.6 IS STM or Tamron Lens");
+        _lensTypeById.put(4144, "Canon EF 40mm f/2.8 STM");
+        _lensTypeById.put(4145, "Canon EF-M 22mm f/2 STM");
+        _lensTypeById.put(4146, "Canon EF-S 18-55mm f/3.5-5.6 IS STM");
+        _lensTypeById.put(4147, "Canon EF-M 11-22mm f/4-5.6 IS STM");
+        _lensTypeById.put(4148, "Canon EF-S 55-250mm f/4-5.6 IS STM");
+        _lensTypeById.put(4149, "Canon EF-M 55-200mm f/4.5-6.3 IS STM");
+        _lensTypeById.put(4150, "Canon EF-S 10-18mm f/4.5-5.6 IS STM");
+        _lensTypeById.put(4152, "Canon EF 24-105mm f/3.5-5.6 IS STM");
+        _lensTypeById.put(4153, "Canon EF-M 15-45mm f/3.5-6.3 IS STM");
+        _lensTypeById.put(4154, "Canon EF-S 24mm f/2.8 STM");
+        _lensTypeById.put(4156, "Canon EF 50mm f/1.8 STM");
+        _lensTypeById.put(36912, "Canon EF-S 18-135mm f/3.5-5.6 IS USM");
+        _lensTypeById.put(65535, "N/A");
+    }
 }
Index: /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,4 +35,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CanonMakernoteDirectory extends Directory
 {
@@ -159,5 +160,5 @@
         public static final int TAG_FOCUS_MODE_1 = OFFSET + 0x07;
         public static final int TAG_UNKNOWN_3 = OFFSET + 0x08;
-        public static final int TAG_UNKNOWN_4 = OFFSET + 0x09;
+        public static final int TAG_RECORD_MODE = OFFSET + 0x09;
         /**
          * 0 = Large
@@ -249,6 +250,6 @@
         public static final int TAG_SHORT_FOCAL_LENGTH = OFFSET + 0x18;
         public static final int TAG_FOCAL_UNITS_PER_MM = OFFSET + 0x19;
-        public static final int TAG_UNKNOWN_9 = OFFSET + 0x1A;
-        public static final int TAG_UNKNOWN_10 = OFFSET + 0x1B;
+        public static final int TAG_MAX_APERTURE = OFFSET + 0x1A;
+        public static final int TAG_MIN_APERTURE = OFFSET + 0x1B;
         /**
          * 0 = Flash Did Not Fire
@@ -257,6 +258,6 @@
         public static final int TAG_FLASH_ACTIVITY = OFFSET + 0x1C;
         public static final int TAG_FLASH_DETAILS = OFFSET + 0x1D;
-        public static final int TAG_UNKNOWN_12 = OFFSET + 0x1E;
-        public static final int TAG_UNKNOWN_13 = OFFSET + 0x1F;
+        public static final int TAG_FOCUS_CONTINUOUS = OFFSET + 0x1E;
+        public static final int TAG_AE_SETTING = OFFSET + 0x1F;
         /**
          * 0 = Focus Mode: Single
@@ -264,4 +265,15 @@
          */
         public static final int TAG_FOCUS_MODE_2 = OFFSET + 0x20;
+
+        public static final int TAG_DISPLAY_APERTURE = OFFSET + 0x21;
+        public static final int TAG_ZOOM_SOURCE_WIDTH = OFFSET + 0x22;
+        public static final int TAG_ZOOM_TARGET_WIDTH = OFFSET + 0x23;
+
+        public static final int TAG_SPOT_METERING_MODE = OFFSET + 0x25;
+        public static final int TAG_PHOTO_EFFECT = OFFSET + 0x26;
+        public static final int TAG_MANUAL_FLASH_OUTPUT = OFFSET + 0x27;
+
+        public static final int TAG_COLOR_TONE = OFFSET + 0x29;
+        public static final int TAG_SRAW_QUALITY = OFFSET + 0x2D;
     }
 
@@ -515,14 +527,22 @@
         _tagNameMap.put(CameraSettings.TAG_UNKNOWN_2, "Unknown Camera Setting 2");
         _tagNameMap.put(CameraSettings.TAG_UNKNOWN_3, "Unknown Camera Setting 3");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_4, "Unknown Camera Setting 4");
+        _tagNameMap.put(CameraSettings.TAG_RECORD_MODE, "Record Mode");
         _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom");
         _tagNameMap.put(CameraSettings.TAG_FOCUS_TYPE, "Focus Type");
         _tagNameMap.put(CameraSettings.TAG_UNKNOWN_7, "Unknown Camera Setting 7");
         _tagNameMap.put(CameraSettings.TAG_LENS_TYPE, "Lens Type");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_9, "Unknown Camera Setting 9");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_10, "Unknown Camera Setting 10");
+        _tagNameMap.put(CameraSettings.TAG_MAX_APERTURE, "Max Aperture");
+        _tagNameMap.put(CameraSettings.TAG_MIN_APERTURE, "Min Aperture");
         _tagNameMap.put(CameraSettings.TAG_FLASH_ACTIVITY, "Flash Activity");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_12, "Unknown Camera Setting 12");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_13, "Unknown Camera Setting 13");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_CONTINUOUS, "Focus Continuous");
+        _tagNameMap.put(CameraSettings.TAG_AE_SETTING, "AE Setting");
+        _tagNameMap.put(CameraSettings.TAG_DISPLAY_APERTURE, "Display Aperture");
+        _tagNameMap.put(CameraSettings.TAG_ZOOM_SOURCE_WIDTH, "Zoom Source Width");
+        _tagNameMap.put(CameraSettings.TAG_ZOOM_TARGET_WIDTH, "Zoom Target Width");
+        _tagNameMap.put(CameraSettings.TAG_SPOT_METERING_MODE, "Spot Metering Mode");
+        _tagNameMap.put(CameraSettings.TAG_PHOTO_EFFECT, "Photo Effect");
+        _tagNameMap.put(CameraSettings.TAG_MANUAL_FLASH_OUTPUT, "Manual Flash Output");
+        _tagNameMap.put(CameraSettings.TAG_COLOR_TONE, "Color Tone");
+        _tagNameMap.put(CameraSettings.TAG_SRAW_QUALITY, "SRAW Quality");
 
         _tagNameMap.put(FocalLength.TAG_WHITE_BALANCE, "White Balance");
@@ -576,5 +596,5 @@
         _tagNameMap.put(AFInfo.TAG_AF_AREA_X_POSITIONS, "AF Area X Positions");
         _tagNameMap.put(AFInfo.TAG_AF_AREA_Y_POSITIONS, "AF Area Y Positions");
-        _tagNameMap.put(AFInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus Count");
+        _tagNameMap.put(AFInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus");
         _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_1, "Primary AF Point 1");
         _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_2, "Primary AF Point 2");
@@ -672,4 +692,10 @@
         // TODO is there some way to drop out 'null' or 'zero' values that are present in the array to reduce the noise?
 
+        if (!(array instanceof int[])) {
+            // no special handling...
+            super.setObjectArray(tagType, array);
+            return;
+        }
+
         // Certain Canon tags contain arrays of values that we split into 'fake' tags as each
         // index in the array has its own meaning and decoding.
@@ -709,7 +735,57 @@
 //                break;
             case TAG_AF_INFO_ARRAY: {
-                int[] ints = (int[])array;
-                for (int i = 0; i < ints.length; i++)
-                    setInt(AFInfo.OFFSET + i, ints[i]);
+                // Notes from Exiftool 10.10 by Phil Harvey, lib\Image\Exiftool\Canon.pm:
+                // Auto-focus information used by many older Canon models. The values in this
+                // record are sequential, and some have variable sizes based on the value of
+                // numafpoints (which may be 1,5,7,9,15,45, or 53). The AFArea coordinates are
+                // given in a system where the image has dimensions given by AFImageWidth and
+                // AFImageHeight, and 0,0 is the image center. The direction of the Y axis
+                // depends on the camera model, with positive Y upwards for EOS models, but
+                // apparently downwards for PowerShot models.
+
+                // AFInfo is another array with 'fake' tags. The first int of the array contains
+                // the number of AF points. Iterate through the array one byte at a time, generally
+                // assuming one byte corresponds to one tag UNLESS certain tag numbers are encountered.
+                // For these, read specific subsequent bytes from the array based on the tag type. The
+                // number of bytes read can vary.
+
+                int[] values = (int[])array;
+                int numafpoints = values[0];
+                int tagnumber = 0;
+                for (int i = 0; i < values.length; i++)
+                {
+                    // These two tags store 'numafpoints' bytes of data in the array
+                    if (AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_AREA_X_POSITIONS ||
+                        AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_AREA_Y_POSITIONS)
+                    {
+                        // There could be incorrect data in the array, so boundary check
+                        if (values.length - 1 >= (i + numafpoints))
+                        {
+                            short[] areaPositions = new short[numafpoints];
+                            for (int j = 0; j < areaPositions.length; j++)
+                                areaPositions[j] = (short)values[i + j];
+
+                            super.setObjectArray(AFInfo.OFFSET + tagnumber, areaPositions);
+                        }
+                        i += numafpoints - 1;   // assume these bytes are processed and skip
+                    }
+                    else if (AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_POINTS_IN_FOCUS)
+                    {
+                        short[] pointsInFocus = new short[((numafpoints + 15) / 16)];
+
+                        // There could be incorrect data in the array, so boundary check
+                        if (values.length - 1 >= (i + pointsInFocus.length))
+                        {
+                            for (int j = 0; j < pointsInFocus.length; j++)
+                                pointsInFocus[j] = (short)values[i + j];
+
+                            super.setObjectArray(AFInfo.OFFSET + tagnumber, pointsInFocus);
+                        }
+                        i += pointsInFocus.length - 1;  // assume these bytes are processed and skip
+                    }
+                    else
+                        super.setObjectArray(AFInfo.OFFSET + tagnumber, values[i]);
+                    tagnumber++;
+                }
                 break;
             }
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,4 +32,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,4 +34,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType1MakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,4 +32,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory>
 {
@@ -68,6 +69,4 @@
             case TAG_SHARPNESS:
                 return getSharpnessDescription();
-            case TAG_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
             case TAG_PREVIEW_THUMBNAIL:
                 return getCasioPreviewThumbnailDescription();
@@ -212,11 +211,4 @@
 
     @Nullable
-    public String getPrintImageMatchingInfoDescription()
-    {
-        // TODO research PIM specification http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-        return _directory.getString(TAG_PRINT_IMAGE_MATCHING_INFO);
-    }
-
-    @Nullable
     public String getSharpnessDescription()
     {
Index: /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,4 +34,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType2MakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -49,4 +49,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class FujifilmMakernoteDescriptor extends TagDescriptor<FujifilmMakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class FujifilmMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,4 +32,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KodakMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -39,4 +39,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDirectory>
 {
@@ -51,6 +52,4 @@
     {
         switch (tagType) {
-            case TAG_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
             case TAG_PROPRIETARY_THUMBNAIL:
                 return getProprietaryThumbnailDataDescription();
@@ -61,10 +60,4 @@
 
     @Nullable
-    public String getPrintImageMatchingInfoDescription()
-    {
-        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
-    }
-
-    @Nullable
     public String getProprietaryThumbnailDataDescription()
     {
Index: /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KyoceraMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,4 +34,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class LeicaMakernoteDescriptor extends TagDescriptor<LeicaMakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class LeicaMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.LeicaType5MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link LeicaType5MakernoteDirectory}.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class LeicaType5MakernoteDescriptor extends TagDescriptor<LeicaType5MakernoteDirectory>
+{
+    public LeicaType5MakernoteDescriptor(@NotNull LeicaType5MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagExposureMode:
+                return getExposureModeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        byte[] values = _directory.getByteArray(TagExposureMode);
+        if (values == null || values.length < 4)
+            return null;
+
+        String join = String.format("%d %d %d %d", values[0], values[1], values[2], values[3]);
+
+        if(join.equals("0 0 0 0"))
+            return "Program AE";
+        else if(join.equals("1 0 0 0"))
+            return "Aperture-priority AE";
+        else if(join.equals("1 1 0 0"))
+            return "Aperture-priority AE (1)";
+        else if(join.equals("2 0 0 0"))
+            return "Shutter speed priority AE";  // guess
+        else if(join.equals("3 0 0 0"))
+            return "Manual";
+        else
+            return String.format("Unknown (%s)", join);
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to certain Leica cameras.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class LeicaType5MakernoteDirectory extends Directory
+{
+    public static final int TagLensModel = 0x0303;
+    public static final int TagOriginalFileName = 0x0407;
+    public static final int TagOriginalDirectory = 0x0408;
+    public static final int TagExposureMode = 0x040d;
+    public static final int TagShotInfo = 0x0410;
+    public static final int TagFilmMode = 0x0412;
+    public static final int TagWbRgbLevels = 0x0413;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagLensModel, "Lens Model");
+        _tagNameMap.put(TagOriginalFileName, "Original File Name");
+        _tagNameMap.put(TagOriginalDirectory, "Original Directory");
+        _tagNameMap.put(TagExposureMode, "Exposure Mode");
+        _tagNameMap.put(TagShotInfo, "Shot Info" );
+        _tagNameMap.put(TagFilmMode, "Film Mode");
+        _tagNameMap.put(TagWbRgbLevels, "WB RGB Levels");
+    }
+
+    public LeicaType5MakernoteDirectory()
+    {
+        this.setDescriptor(new LeicaType5MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Leica Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,4 +44,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType1MakernoteDescriptor extends TagDescriptor<NikonType1MakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -40,4 +40,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType1MakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +37,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2MakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -45,4 +45,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType2MakernoteDirectory extends Directory
 {
@@ -760,5 +761,5 @@
     public static final int TAG_UNKNOWN_50 = 0x00BD;
     public static final int TAG_UNKNOWN_51 = 0x0103;
-    public static final int TAG_PRINT_IM = 0x0E00;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
     /**
@@ -893,5 +894,5 @@
         _tagNameMap.put(TAG_UNKNOWN_50, "Unknown 50");
         _tagNameMap.put(TAG_UNKNOWN_51, "Unknown 51");
-        _tagNameMap.put(TAG_PRINT_IM, "Print IM");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print IM");
         _tagNameMap.put(TAG_UNKNOWN_52, "Unknown 52");
         _tagNameMap.put(TAG_UNKNOWN_53, "Unknown 53");
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java	(revision 13061)
@@ -41,4 +41,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusCameraSettingsMakernoteDescriptor extends TagDescriptor<OlympusCameraSettingsMakernoteDirectory>
 {
@@ -143,4 +144,6 @@
             case TagArtFilterEffect:
                 return getArtFilterEffectDescription();
+            case TagColorCreatorEffect:
+                return getColorCreatorEffectDescription();
 
             case TagDriveMode:
@@ -407,6 +410,8 @@
         int p4 = (int)(values[index + 3].doubleValue() * 100);
 
+        if(p1 + p2 + p3 + p4 == 0)
+            return "n/a";
+
         return String.format("(%d%%,%d%%) (%d%%,%d%%)", p1, p2, p3, p4);
-
     }
 
@@ -1019,10 +1024,8 @@
         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("; ");
+            if (i == 0 || i == 4 || i == 8 || i == 12 || i == 16 || i == 20 || i == 24)
+                sb.append(_toneLevelType.get(values[i])).append("; ");
+            else
+                sb.append(values[i]).append("; ");
         }
 
@@ -1034,5 +1037,5 @@
     {
         int[] values = _directory.getIntArray(TagArtFilterEffect);
-        if (values == null || values.length == 0)
+        if (values == null)
             return null;
 
@@ -1040,5 +1043,7 @@
         for (int i = 0; i < values.length; i++) {
             if (i == 0) {
-                sb.append((_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]"));
+                sb.append((_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]")).append("; ");
+            } else if (i == 3) {
+                sb.append("Partial Color ").append(values[i]).append("; ");
             } else if (i == 4) {
                 switch (values[i]) {
@@ -1068,8 +1073,51 @@
                         break;
                 }
+                sb.append("; ");
+            } else if (i == 6) {
+                switch (values[i]) {
+                    case 0:
+                        sb.append("No Color Filter");
+                        break;
+                    case 1:
+                        sb.append("Yellow Color Filter");
+                        break;
+                    case 2:
+                        sb.append("Orange Color Filter");
+                        break;
+                    case 3:
+                        sb.append("Red Color Filter");
+                        break;
+                    case 4:
+                        sb.append("Green Color Filter");
+                        break;
+                    default:
+                        sb.append("Unknown (").append(values[i]).append(")");
+                        break;
+                }
+                sb.append("; ");
             } else {
-                sb.append(values[i]);
+                sb.append(values[i]).append("; ");
             }
-            sb.append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getColorCreatorEffectDescription()
+    {
+        int[] values = _directory.getIntArray(TagColorCreatorEffect);
+        if (values == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0) {
+                sb.append("Color ").append(values[i]).append("; ");
+            } else if (i == 3) {
+                sb.append("Strength ").append(values[i]).append("; ");
+            } else {
+                sb.append(values[i]).append("; ");
+            }
         }
 
@@ -1294,4 +1342,5 @@
     }
 
+    @Nullable
     private String getFiltersDescription(int tagId)
     {
@@ -1312,4 +1361,5 @@
     }
 
+    private static final HashMap<Integer, String> _toneLevelType = new HashMap<Integer, String>();
     // ArtFilter, ArtFilterEffect and MagicFilter values
     private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>();
@@ -1354,4 +1404,9 @@
         _filters.put(40, "Partial Color II");
         _filters.put(41, "Partial Color III");
+
+        _toneLevelType.put(0, "0");
+        _toneLevelType.put(-31999, "Highlights ");
+        _toneLevelType.put(-31998, "Shadows ");
+        _toneLevelType.put(-31997, "Midtones ");
     }
 }
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java	(revision 13061)
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusCameraSettingsMakernoteDirectory extends Directory
 {
@@ -89,4 +90,5 @@
     public static final int TagToneLevel = 0x52e;
     public static final int TagArtFilterEffect = 0x52f;
+    public static final int TagColorCreatorEffect = 0x532;
 
     public static final int TagDriveMode = 0x600;
@@ -162,4 +164,5 @@
         _tagNameMap.put(TagToneLevel, "Tone Level");
         _tagNameMap.put(TagArtFilterEffect, "Art Filter Effect");
+        _tagNameMap.put(TagColorCreatorEffect, "Color Creator Effect");
 
         _tagNameMap.put(TagDriveMode, "Drive Mode");
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java	(revision 13061)
@@ -40,4 +40,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusEquipmentMakernoteDescriptor extends TagDescriptor<OlympusEquipmentMakernoteDirectory>
 {
@@ -53,27 +54,29 @@
         switch (tagType) {
             case TAG_EQUIPMENT_VERSION:
-                return GetEquipmentVersionDescription();
+                return getEquipmentVersionDescription();
+            case TAG_CAMERA_TYPE_2:
+                return getCameraType2Description();
             case TAG_FOCAL_PLANE_DIAGONAL:
-                return GetFocalPlaneDiagonalDescription();
+                return getFocalPlaneDiagonalDescription();
             case TAG_BODY_FIRMWARE_VERSION:
-                return GetBodyFirmwareVersionDescription();
+                return getBodyFirmwareVersionDescription();
             case TAG_LENS_TYPE:
-                return GetLensTypeDescription();
+                return getLensTypeDescription();
             case TAG_LENS_FIRMWARE_VERSION:
-                return GetLensFirmwareVersionDescription();
+                return getLensFirmwareVersionDescription();
             case TAG_MAX_APERTURE_AT_MIN_FOCAL:
-                return GetMaxApertureAtMinFocalDescription();
+                return getMaxApertureAtMinFocalDescription();
             case TAG_MAX_APERTURE_AT_MAX_FOCAL:
-                return GetMaxApertureAtMaxFocalDescription();
+                return getMaxApertureAtMaxFocalDescription();
             case TAG_MAX_APERTURE:
-                return GetMaxApertureDescription();
+                return getMaxApertureDescription();
             case TAG_LENS_PROPERTIES:
-                return GetLensPropertiesDescription();
+                return getLensPropertiesDescription();
             case TAG_EXTENDER:
-                return GetExtenderDescription();
+                return getExtenderDescription();
             case TAG_FLASH_TYPE:
-                return GetFlashTypeDescription();
+                return getFlashTypeDescription();
             case TAG_FLASH_MODEL:
-                return GetFlashModelDescription();
+                return getFlashModelDescription();
             default:
                 return super.getDescription(tagType);
@@ -82,5 +85,5 @@
 
     @Nullable
-    public String GetEquipmentVersionDescription()
+    public String getEquipmentVersionDescription()
     {
         return getVersionBytesDescription(TAG_EQUIPMENT_VERSION, 4);
@@ -88,5 +91,18 @@
 
     @Nullable
-    public String GetFocalPlaneDiagonalDescription()
+    public String getCameraType2Description()
+    {
+        String cameratype = _directory.getString(TAG_CAMERA_TYPE_2);
+        if(cameratype == null)
+            return null;
+
+        if(OlympusMakernoteDirectory.OlympusCameraTypes.containsKey(cameratype))
+            return OlympusMakernoteDirectory.OlympusCameraTypes.get(cameratype);
+
+        return cameratype;
+    }
+
+    @Nullable
+    public String getFocalPlaneDiagonalDescription()
     {
         return _directory.getString(TAG_FOCAL_PLANE_DIAGONAL) + " mm";
@@ -94,5 +110,5 @@
 
     @Nullable
-    public String GetBodyFirmwareVersionDescription()
+    public String getBodyFirmwareVersionDescription()
     {
         Integer value = _directory.getInteger(TAG_BODY_FIRMWARE_VERSION);
@@ -107,5 +123,5 @@
 
     @Nullable
-    public String GetLensTypeDescription()
+    public String getLensTypeDescription()
     {
         String str = _directory.getString(TAG_LENS_TYPE);
@@ -140,5 +156,5 @@
 
     @Nullable
-    public String GetLensFirmwareVersionDescription()
+    public String getLensFirmwareVersionDescription()
     {
         Integer value = _directory.getInteger(TAG_LENS_FIRMWARE_VERSION);
@@ -153,5 +169,5 @@
 
     @Nullable
-    public String GetMaxApertureAtMinFocalDescription()
+    public String getMaxApertureAtMinFocalDescription()
     {
         Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MIN_FOCAL);
@@ -164,5 +180,5 @@
 
     @Nullable
-    public String GetMaxApertureAtMaxFocalDescription()
+    public String getMaxApertureAtMaxFocalDescription()
     {
         Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MAX_FOCAL);
@@ -175,5 +191,5 @@
 
     @Nullable
-    public String GetMaxApertureDescription()
+    public String getMaxApertureDescription()
     {
         Integer value = _directory.getInteger(TAG_MAX_APERTURE);
@@ -191,5 +207,5 @@
 
     @Nullable
-    public String GetLensPropertiesDescription()
+    public String getLensPropertiesDescription()
     {
         Integer value = _directory.getInteger(TAG_LENS_PROPERTIES);
@@ -201,5 +217,5 @@
 
     @Nullable
-    public String GetExtenderDescription()
+    public String getExtenderDescription()
     {
         String str = _directory.getString(TAG_EXTENDER);
@@ -234,5 +250,5 @@
 
     @Nullable
-    public String GetFlashTypeDescription()
+    public String getFlashTypeDescription()
     {
         return getIndexedDescription(TAG_FLASH_TYPE,
@@ -241,5 +257,5 @@
 
     @Nullable
-    public String GetFlashModelDescription()
+    public String getFlashModelDescription()
     {
         return getIndexedDescription(TAG_FLASH_MODEL,
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java	(revision 13061)
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusEquipmentMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusFocusInfoMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusFocusInfoMakernoteDirectory}.
+ * <p>
+ * Some Description functions 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusFocusInfoMakernoteDescriptor extends TagDescriptor<OlympusFocusInfoMakernoteDirectory>
+{
+    public OlympusFocusInfoMakernoteDescriptor(@NotNull OlympusFocusInfoMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagFocusInfoVersion:
+                return getFocusInfoVersionDescription();
+            case TagAutoFocus:
+                return getAutoFocusDescription();
+            case TagFocusDistance:
+                return getFocusDistanceDescription();
+            case TagAfPoint:
+                return getAfPointDescription();
+            case TagExternalFlash:
+                return getExternalFlashDescription();
+            case TagExternalFlashBounce:
+                return getExternalFlashBounceDescription();
+            case TagExternalFlashZoom:
+                return getExternalFlashZoomDescription();
+            case TagManualFlash:
+                return getManualFlashDescription();
+            case TagMacroLed:
+                return getMacroLedDescription();
+            case TagSensorTemperature:
+                return getSensorTemperatureDescription();
+            case TagImageStabilization:
+                return getImageStabilizationDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFocusInfoVersionDescription()
+    {
+        return getVersionBytesDescription(TagFocusInfoVersion, 4);
+    }
+
+    @Nullable
+    public String getAutoFocusDescription()
+    {
+        return getIndexedDescription(TagAutoFocus,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getFocusDistanceDescription()
+    {
+        Rational value = _directory.getRational(TagFocusDistance);
+        if (value == null)
+            return "inf";
+        if (value.getNumerator() == 0xFFFFFFFFL || value.getNumerator() == 0x00000000L)
+            return "inf";
+
+        return value.getNumerator() / 1000.0 + " m";
+    }
+
+    @Nullable
+    public String getAfPointDescription()
+    {
+        Integer value = _directory.getInteger(TagAfPoint);
+        if (value == null)
+            return null;
+
+        return value.toString();
+    }
+
+    @Nullable
+    public String getExternalFlashDescription()
+    {
+        int[] values = _directory.getIntArray(TagExternalFlash);
+        if (values == null || values.length < 2)
+            return null;
+
+        String join = String.format("%d %d", (short)values[0], (short)values[1]);
+
+        if(join.equals("0 0"))
+            return "Off";
+        else if(join.equals("1 0"))
+            return "On";
+        else
+            return "Unknown (" + join + ")";
+    }
+
+    @Nullable
+    public String getExternalFlashBounceDescription()
+    {
+        return getIndexedDescription(TagExternalFlashBounce,
+                "Bounce or Off", "Direct");
+    }
+
+    @Nullable
+    public String getExternalFlashZoomDescription()
+    {
+        int[] values = _directory.getIntArray(TagExternalFlashZoom);
+        if (values == null)
+        {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagExternalFlashZoom);
+            if(value == null)
+                return null;
+
+            values = new int[1];
+            values[0] = value;
+        }
+
+        if (values.length == 0)
+            return null;
+
+        String join = String.format("%d", (short)values[0]);
+        if(values.length > 1)
+            join += " " + String.format("%d", (short)values[1]);
+
+        if(join.equals("0"))
+            return "Off";
+        else if(join.equals("1"))
+            return "On";
+        else if(join.equals("0 0"))
+            return "Off";
+        else if(join.equals("1 0"))
+            return "On";
+        else
+            return "Unknown (" + join + ")";
+
+    }
+
+    @Nullable
+    public String getManualFlashDescription()
+    {
+        int[] values = _directory.getIntArray(TagManualFlash);
+        if (values == null)
+            return null;
+
+        if ((short)values[0] == 0)
+            return "Off";
+
+        if ((short)values[1] == 1)
+            return "Full";
+        return "On (1/" + (short)values[1] + " strength)";
+    }
+
+    @Nullable
+    public String getMacroLedDescription()
+    {
+        return getIndexedDescription(TagMacroLed,
+                "Off", "On");
+    }
+
+    /// <remarks>
+    /// <para>TODO: Complete when Camera Model is available.</para>
+    /// <para>There are differences in how to interpret this tag that can only be reconciled by knowing the model.</para>
+    /// </remarks>
+    @Nullable
+    public String getSensorTemperatureDescription()
+    {
+        return _directory.getString(TagSensorTemperature);
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        byte[] values = _directory.getByteArray(TagImageStabilization);
+        if (values == null)
+            return null;
+
+        if((values[0] | values[1] | values[2] | values[3]) == 0x0)
+            return "Off";
+        return "On, " + ((values[43] & 1) > 0 ? "Mode 1" : "Mode 2");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,110 @@
+/*
+ * 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 focus info 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusFocusInfoMakernoteDirectory extends Directory
+{
+    public static final int TagFocusInfoVersion = 0x0000;
+    public static final int TagAutoFocus = 0x0209;
+    public static final int TagSceneDetect = 0x0210;
+    public static final int TagSceneArea = 0x0211;
+    public static final int TagSceneDetectData = 0x0212;
+
+    public static final int TagZoomStepCount = 0x0300;
+    public static final int TagFocusStepCount = 0x0301;
+    public static final int TagFocusStepInfinity = 0x0303;
+    public static final int TagFocusStepNear = 0x0304;
+    public static final int TagFocusDistance = 0x0305;
+    public static final int TagAfPoint = 0x0308;
+    // 0x031a Continuous AF parameters?
+    public static final int TagAfInfo = 0x0328;    // ifd
+
+    public static final int TagExternalFlash = 0x1201;
+    public static final int TagExternalFlashGuideNumber = 0x1203;
+    public static final int TagExternalFlashBounce = 0x1204;
+    public static final int TagExternalFlashZoom = 0x1205;
+    public static final int TagInternalFlash = 0x1208;
+    public static final int TagManualFlash = 0x1209;
+    public static final int TagMacroLed = 0x120A;
+
+    public static final int TagSensorTemperature = 0x1500;
+
+    public static final int TagImageStabilization = 0x1600;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagFocusInfoVersion, "Focus Info Version");
+        _tagNameMap.put(TagAutoFocus, "Auto Focus");
+        _tagNameMap.put(TagSceneDetect, "Scene Detect");
+        _tagNameMap.put(TagSceneArea, "Scene Area");
+        _tagNameMap.put(TagSceneDetectData, "Scene Detect Data");
+        _tagNameMap.put(TagZoomStepCount, "Zoom Step Count");
+        _tagNameMap.put(TagFocusStepCount, "Focus Step Count");
+        _tagNameMap.put(TagFocusStepInfinity, "Focus Step Infinity");
+        _tagNameMap.put(TagFocusStepNear, "Focus Step Near");
+        _tagNameMap.put(TagFocusDistance, "Focus Distance");
+        _tagNameMap.put(TagAfPoint, "AF Point");
+        _tagNameMap.put(TagAfInfo, "AF Info");
+        _tagNameMap.put(TagExternalFlash, "External Flash");
+        _tagNameMap.put(TagExternalFlashGuideNumber, "External Flash Guide Number");
+        _tagNameMap.put(TagExternalFlashBounce, "External Flash Bounce");
+        _tagNameMap.put(TagExternalFlashZoom, "External Flash Zoom");
+        _tagNameMap.put(TagInternalFlash, "Internal Flash");
+        _tagNameMap.put(TagManualFlash, "Manual Flash");
+        _tagNameMap.put(TagMacroLed, "Macro LED");
+        _tagNameMap.put(TagSensorTemperature, "Sensor Temperature");
+        _tagNameMap.put(TagImageStabilization, "Image Stabilization");
+    }
+
+    public OlympusFocusInfoMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusFocusInfoMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Focus Info";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusImageProcessingMakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.33 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusImageProcessingMakernoteDescriptor extends TagDescriptor<OlympusImageProcessingMakernoteDirectory>
+{
+    public OlympusImageProcessingMakernoteDescriptor(@NotNull OlympusImageProcessingMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagImageProcessingVersion:
+                return getImageProcessingVersionDescription();
+            case TagColorMatrix:
+                return getColorMatrixDescription();
+            case TagNoiseReduction2:
+                return getNoiseReduction2Description();
+            case TagDistortionCorrection2:
+                return getDistortionCorrection2Description();
+            case TagShadingCompensation2:
+                return getShadingCompensation2Description();
+            case TagMultipleExposureMode:
+                return getMultipleExposureModeDescription();
+            case TagAspectRatio:
+                return getAspectRatioDescription();
+            case TagKeystoneCompensation:
+                return getKeystoneCompensationDescription();
+            case TagKeystoneDirection:
+                return getKeystoneDirectionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getImageProcessingVersionDescription()
+    {
+        return getVersionBytesDescription(TagImageProcessingVersion, 4);
+    }
+
+    @Nullable
+    public String getColorMatrixDescription()
+    {
+        int[] obj = _directory.getIntArray(TagColorMatrix);
+        if (obj == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < obj.length; i++) {
+            if (i != 0)
+                sb.append(" ");
+            sb.append((short)obj[i]);
+        }
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getNoiseReduction2Description()
+    {
+        Integer value = _directory.getInteger(TagNoiseReduction2);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        short v = value.shortValue();
+
+        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), ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getDistortionCorrection2Description()
+    {
+        return getIndexedDescription(TagDistortionCorrection2, "Off", "On");
+    }
+
+    @Nullable
+    public String getShadingCompensation2Description()
+    {
+        return getIndexedDescription(TagShadingCompensation2, "Off", "On");
+    }
+
+    @Nullable
+    public String getMultipleExposureModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagMultipleExposureMode);
+        if (values == null)
+        {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagMultipleExposureMode);
+            if(value == null)
+                return null;
+
+            values = new int[1];
+            values[0] = value;
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        switch ((short)values[0])
+        {
+            case 0:
+                sb.append("Off");
+                break;
+            case 2:
+                sb.append("On (2 frames)");
+                break;
+            case 3:
+                sb.append("On (3 frames)");
+                break;
+            default:
+                sb.append("Unknown (").append((short)values[0]).append(")");
+                break;
+        }
+
+        if (values.length > 1)
+            sb.append("; ").append((short)values[1]);
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getAspectRatioDescription()
+    {
+        byte[] values = _directory.getByteArray(TagAspectRatio);
+        if (values == null || values.length < 2)
+            return null;
+
+        String join = String.format("%d %d", values[0], values[1]);
+
+        String ret;
+        if(join.equals("1 1"))
+            ret = "4:3";
+        else if(join.equals("1 4"))
+            ret = "1:1";
+        else if(join.equals("2 1"))
+            ret = "3:2 (RAW)";
+        else if(join.equals("2 2"))
+            ret = "3:2";
+        else if(join.equals("3 1"))
+            ret = "16:9 (RAW)";
+        else if(join.equals("3 3"))
+            ret = "16:9";
+        else if(join.equals("4 1"))
+            ret = "1:1 (RAW)";
+        else if(join.equals("4 4"))
+            ret = "6:6";
+        else if(join.equals("5 5"))
+            ret = "5:4";
+        else if(join.equals("6 6"))
+            ret = "7:6";
+        else if(join.equals("7 7"))
+            ret = "6:5";
+        else if(join.equals("8 8"))
+            ret = "7:5";
+        else if(join.equals("9 1"))
+            ret = "3:4 (RAW)";
+        else if(join.equals("9 9"))
+            ret = "3:4";
+        else
+            ret = "Unknown (" + join + ")";
+
+        return ret;
+    }
+
+    @Nullable
+    public String getKeystoneCompensationDescription()
+    {
+        byte[] values = _directory.getByteArray(TagKeystoneCompensation);
+        if (values == null || values.length < 2)
+            return null;
+
+        String join = String.format("%d %d", values[0], values[1]);
+
+        String ret;
+        if(join.equals("0 0"))
+            ret = "Off";
+        else if(join.equals("0 1"))
+            ret = "On";
+        else
+            ret = "Unknown (" + join + ")";
+
+        return ret;
+    }
+
+    @Nullable
+    public String getKeystoneDirectionDescription()
+    {
+        return getIndexedDescription(TagKeystoneDirection, "Vertical", "Horizontal");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,212 @@
+/*
+ * 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 image processing 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusImageProcessingMakernoteDirectory extends Directory
+{
+    public static final int TagImageProcessingVersion = 0x0000;
+    public static final int TagWbRbLevels = 0x0100;
+    // 0x0101 - in-camera AutoWB unless it is all 0's or all 256's (ref IB)
+    public static final int TagWbRbLevels3000K = 0x0102;
+    public static final int TagWbRbLevels3300K = 0x0103;
+    public static final int TagWbRbLevels3600K = 0x0104;
+    public static final int TagWbRbLevels3900K = 0x0105;
+    public static final int TagWbRbLevels4000K = 0x0106;
+    public static final int TagWbRbLevels4300K = 0x0107;
+    public static final int TagWbRbLevels4500K = 0x0108;
+    public static final int TagWbRbLevels4800K = 0x0109;
+    public static final int TagWbRbLevels5300K = 0x010a;
+    public static final int TagWbRbLevels6000K = 0x010b;
+    public static final int TagWbRbLevels6600K = 0x010c;
+    public static final int TagWbRbLevels7500K = 0x010d;
+    public static final int TagWbRbLevelsCwB1 = 0x010e;
+    public static final int TagWbRbLevelsCwB2 = 0x010f;
+    public static final int TagWbRbLevelsCwB3 = 0x0110;
+    public static final int TagWbRbLevelsCwB4 = 0x0111;
+    public static final int TagWbGLevel3000K = 0x0113;
+    public static final int TagWbGLevel3300K = 0x0114;
+    public static final int TagWbGLevel3600K = 0x0115;
+    public static final int TagWbGLevel3900K = 0x0116;
+    public static final int TagWbGLevel4000K = 0x0117;
+    public static final int TagWbGLevel4300K = 0x0118;
+    public static final int TagWbGLevel4500K = 0x0119;
+    public static final int TagWbGLevel4800K = 0x011a;
+    public static final int TagWbGLevel5300K = 0x011b;
+    public static final int TagWbGLevel6000K = 0x011c;
+    public static final int TagWbGLevel6600K = 0x011d;
+    public static final int TagWbGLevel7500K = 0x011e;
+    public static final int TagWbGLevel = 0x011f;
+    // 0x0121 = WB preset for flash (about 6000K) (ref IB)
+    // 0x0125 = WB preset for underwater (ref IB)
+
+    public static final int TagColorMatrix = 0x0200;
+    // color matrices (ref 11):
+    // 0x0201-0x020d are sRGB color matrices
+    // 0x020e-0x021a are Adobe RGB color matrices
+    // 0x021b-0x0227 are ProPhoto RGB color matrices
+    // 0x0228 and 0x0229 are ColorMatrix for E-330
+    // 0x0250-0x0252 are sRGB color matrices
+    // 0x0253-0x0255 are Adobe RGB color matrices
+    // 0x0256-0x0258 are ProPhoto RGB color matrices
+
+    public static final int TagEnhancer = 0x0300;
+    public static final int TagEnhancerValues = 0x0301;
+    public static final int TagCoringFilter = 0x0310;
+    public static final int TagCoringValues = 0x0311;
+    public static final int TagBlackLevel2 = 0x0600;
+    public static final int TagGainBase = 0x0610;
+    public static final int TagValidBits = 0x0611;
+    public static final int TagCropLeft = 0x0612;
+    public static final int TagCropTop = 0x0613;
+    public static final int TagCropWidth = 0x0614;
+    public static final int TagCropHeight = 0x0615;
+    public static final int TagUnknownBlock1 = 0x0635;
+    public static final int TagUnknownBlock2 = 0x0636;
+
+    // 0x0800 LensDistortionParams, float[9] (ref 11)
+    // 0x0801 LensShadingParams, int16u[16] (ref 11)
+    public static final int TagSensorCalibration = 0x0805;
+
+    public static final int TagNoiseReduction2 = 0x1010;
+    public static final int TagDistortionCorrection2 = 0x1011;
+    public static final int TagShadingCompensation2 = 0x1012;
+    public static final int TagMultipleExposureMode = 0x101c;
+    public static final int TagUnknownBlock3 = 0x1103;
+    public static final int TagUnknownBlock4 = 0x1104;
+    public static final int TagAspectRatio = 0x1112;
+    public static final int TagAspectFrame = 0x1113;
+    public static final int TagFacesDetected = 0x1200;
+    public static final int TagFaceDetectArea = 0x1201;
+    public static final int TagMaxFaces = 0x1202;
+    public static final int TagFaceDetectFrameSize = 0x1203;
+    public static final int TagFaceDetectFrameCrop = 0x1207;
+    public static final int TagCameraTemperature = 0x1306;
+
+    public static final int TagKeystoneCompensation = 0x1900;
+    public static final int TagKeystoneDirection = 0x1901;
+    // 0x1905 - focal length (PH, E-M1)
+    public static final int TagKeystoneValue = 0x1906;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagImageProcessingVersion, "Image Processing Version");
+        _tagNameMap.put(TagWbRbLevels, "WB RB Levels");
+        _tagNameMap.put(TagWbRbLevels3000K, "WB RB Levels 3000K");
+        _tagNameMap.put(TagWbRbLevels3300K, "WB RB Levels 3300K");
+        _tagNameMap.put(TagWbRbLevels3600K, "WB RB Levels 3600K");
+        _tagNameMap.put(TagWbRbLevels3900K, "WB RB Levels 3900K");
+        _tagNameMap.put(TagWbRbLevels4000K, "WB RB Levels 4000K");
+        _tagNameMap.put(TagWbRbLevels4300K, "WB RB Levels 4300K");
+        _tagNameMap.put(TagWbRbLevels4500K, "WB RB Levels 4500K");
+        _tagNameMap.put(TagWbRbLevels4800K, "WB RB Levels 4800K");
+        _tagNameMap.put(TagWbRbLevels5300K, "WB RB Levels 5300K");
+        _tagNameMap.put(TagWbRbLevels6000K, "WB RB Levels 6000K");
+        _tagNameMap.put(TagWbRbLevels6600K, "WB RB Levels 6600K");
+        _tagNameMap.put(TagWbRbLevels7500K, "WB RB Levels 7500K");
+        _tagNameMap.put(TagWbRbLevelsCwB1, "WB RB Levels CWB1");
+        _tagNameMap.put(TagWbRbLevelsCwB2, "WB RB Levels CWB2");
+        _tagNameMap.put(TagWbRbLevelsCwB3, "WB RB Levels CWB3");
+        _tagNameMap.put(TagWbRbLevelsCwB4, "WB RB Levels CWB4");
+        _tagNameMap.put(TagWbGLevel3000K, "WB G Level 3000K");
+        _tagNameMap.put(TagWbGLevel3300K, "WB G Level 3300K");
+        _tagNameMap.put(TagWbGLevel3600K, "WB G Level 3600K");
+        _tagNameMap.put(TagWbGLevel3900K, "WB G Level 3900K");
+        _tagNameMap.put(TagWbGLevel4000K, "WB G Level 4000K");
+        _tagNameMap.put(TagWbGLevel4300K, "WB G Level 4300K");
+        _tagNameMap.put(TagWbGLevel4500K, "WB G Level 4500K");
+        _tagNameMap.put(TagWbGLevel4800K, "WB G Level 4800K");
+        _tagNameMap.put(TagWbGLevel5300K, "WB G Level 5300K");
+        _tagNameMap.put(TagWbGLevel6000K, "WB G Level 6000K");
+        _tagNameMap.put(TagWbGLevel6600K, "WB G Level 6600K");
+        _tagNameMap.put(TagWbGLevel7500K, "WB G Level 7500K");
+        _tagNameMap.put(TagWbGLevel, "WB G Level");
+
+        _tagNameMap.put(TagColorMatrix, "Color Matrix");
+
+        _tagNameMap.put(TagEnhancer, "Enhancer");
+        _tagNameMap.put(TagEnhancerValues, "Enhancer Values");
+        _tagNameMap.put(TagCoringFilter, "Coring Filter");
+        _tagNameMap.put(TagCoringValues, "Coring Values");
+        _tagNameMap.put(TagBlackLevel2, "Black Level 2");
+        _tagNameMap.put(TagGainBase, "Gain Base");
+        _tagNameMap.put(TagValidBits, "Valid Bits");
+        _tagNameMap.put(TagCropLeft, "Crop Left");
+        _tagNameMap.put(TagCropTop, "Crop Top");
+        _tagNameMap.put(TagCropWidth, "Crop Width");
+        _tagNameMap.put(TagCropHeight, "Crop Height");
+        _tagNameMap.put(TagUnknownBlock1, "Unknown Block 1");
+        _tagNameMap.put(TagUnknownBlock2, "Unknown Block 2");
+
+        _tagNameMap.put(TagSensorCalibration, "Sensor Calibration");
+
+        _tagNameMap.put(TagNoiseReduction2, "Noise Reduction 2");
+        _tagNameMap.put(TagDistortionCorrection2, "Distortion Correction 2");
+        _tagNameMap.put(TagShadingCompensation2, "Shading Compensation 2");
+        _tagNameMap.put(TagMultipleExposureMode, "Multiple Exposure Mode");
+        _tagNameMap.put(TagUnknownBlock3, "Unknown Block 3");
+        _tagNameMap.put(TagUnknownBlock4, "Unknown Block 4");
+        _tagNameMap.put(TagAspectRatio, "Aspect Ratio");
+        _tagNameMap.put(TagAspectFrame, "Aspect Frame");
+        _tagNameMap.put(TagFacesDetected, "Faces Detected");
+        _tagNameMap.put(TagFaceDetectArea, "Face Detect Area");
+        _tagNameMap.put(TagMaxFaces, "Max Faces");
+        _tagNameMap.put(TagFaceDetectFrameSize, "Face Detect Frame Size");
+        _tagNameMap.put(TagFaceDetectFrameCrop, "Face Detect Frame Crop");
+        _tagNameMap.put(TagCameraTemperature , "Camera Temperature");
+        _tagNameMap.put(TagKeystoneCompensation, "Keystone Compensation");
+        _tagNameMap.put(TagKeystoneDirection, "Keystone Direction");
+        _tagNameMap.put(TagKeystoneValue, "Keystone Value");
+    }
+
+    public OlympusImageProcessingMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusImageProcessingMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Image Processing";
+    }
+
+    @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 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,4 +21,6 @@
 package com.drew.metadata.exif.makernotes;
 
+import com.drew.imaging.PhotographicConversions;
+import com.drew.lang.Rational;
 import com.drew.lang.DateUtil;
 import com.drew.lang.annotations.NotNull;
@@ -36,4 +38,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDirectory>
 {
@@ -66,8 +69,20 @@
             case TAG_BW_MODE:
                 return getBWModeDescription();
-            case TAG_DIGI_ZOOM_RATIO:
-                return getDigiZoomRatioDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_FOCAL_PLANE_DIAGONAL:
+                return getFocalPlaneDiagonalDescription();
+            case TAG_CAMERA_TYPE:
+                return getCameraTypeDescription();
             case TAG_CAMERA_ID:
                 return getCameraIdDescription();
+            case TAG_ONE_TOUCH_WB:
+                return getOneTouchWbDescription();
+            case TAG_SHUTTER_SPEED_VALUE:
+                return getShutterSpeedDescription();
+            case TAG_ISO_VALUE:
+                return getIsoValueDescription();
+            case TAG_APERTURE_VALUE:
+                return getApertureValueDescription();
             case TAG_FLASH_MODE:
                 return getFlashModeDescription();
@@ -78,4 +93,16 @@
             case TAG_SHARPNESS:
                 return getSharpnessDescription();
+            case TAG_COLOUR_MATRIX:
+                return getColorMatrixDescription();
+            case TAG_WB_MODE:
+                return getWbModeDescription();
+            case TAG_RED_BALANCE:
+                return getRedBalanceDescription();
+            case TAG_BLUE_BALANCE:
+                return getBlueBalanceDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_PREVIEW_IMAGE_VALID:
+                return getPreviewImageValidDescription();
 
             case CameraSettings.TAG_EXPOSURE_MODE:
@@ -102,5 +129,5 @@
                 return getMacroModeCameraSettingDescription();
             case CameraSettings.TAG_DIGITAL_ZOOM:
-                return getDigitalZoomDescription();
+                return getDigitalZoomCameraSettingDescription();
             case CameraSettings.TAG_EXPOSURE_COMPENSATION:
                 return getExposureCompensationDescription();
@@ -138,5 +165,5 @@
                 return getSaturationDescription();
             case CameraSettings.TAG_CONTRAST:
-                return getContrastDescription();
+                return getContrastCameraSettingDescription();
             case CameraSettings.TAG_SHARPNESS:
                 return getSharpnessCameraSettingDescription();
@@ -305,5 +332,5 @@
 
     @Nullable
-    public String getDigitalZoomDescription()
+    public String getDigitalZoomCameraSettingDescription()
     {
         return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "Off", "Electronic magnification", "Digital zoom 2x");
@@ -468,5 +495,5 @@
 
     @Nullable
-    public String getContrastDescription()
+    public String getContrastCameraSettingDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_CONTRAST);
@@ -647,4 +674,91 @@
 
     @Nullable
+    public String getColorMatrixDescription()
+    {
+        int[] obj = _directory.getIntArray(TAG_COLOUR_MATRIX);
+        if (obj == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < obj.length; i++) {
+            sb.append((short)obj[i]);
+            if (i < obj.length - 1)
+                sb.append(" ");
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    @Nullable
+    public String getWbModeDescription()
+    {
+        int[] obj = _directory.getIntArray(TAG_WB_MODE);
+        if (obj == null)
+            return null;
+
+        String val = String.format("%d %d", obj[0], obj[1]);
+
+        if(val.equals("1 0"))
+            return "Auto";
+        else if(val.equals("1 2"))
+            return "Auto (2)";
+        else if(val.equals("1 4"))
+            return "Auto (4)";
+        else if(val.equals("2 2"))
+            return "3000 Kelvin";
+        else if(val.equals("2 3"))
+            return "3700 Kelvin";
+        else if(val.equals("2 4"))
+            return "4000 Kelvin";
+        else if(val.equals("2 5"))
+            return "4500 Kelvin";
+        else if(val.equals("2 6"))
+            return "5500 Kelvin";
+        else if(val.equals("2 7"))
+            return "6500 Kelvin";
+        else if(val.equals("2 8"))
+            return "7500 Kelvin";
+        else if(val.equals("3 0"))
+            return "One-touch";
+        else
+            return "Unknown " + val;
+    }
+
+    @Nullable
+    public String getRedBalanceDescription()
+    {
+        int[] values = _directory.getIntArray(TAG_RED_BALANCE);
+        if (values == null)
+            return null;
+
+        short value = (short)values[0];
+
+        return String.valueOf((double)value/256d);
+    }
+
+    @Nullable
+    public String getBlueBalanceDescription()
+    {
+        int[] values = _directory.getIntArray(TAG_BLUE_BALANCE);
+        if (values == null)
+            return null;
+
+        short value = (short)values[0];
+
+        return String.valueOf((double)value/256d);
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "High", "Normal", "Low");
+    }
+
+    @Nullable
+    public String getPreviewImageValidDescription()
+    {
+        return getIndexedDescription(TAG_PREVIEW_IMAGE_VALID, "No", "Yes");
+    }
+
+    @Nullable
     public String getFocusModeDescription()
     {
@@ -665,7 +779,34 @@
 
     @Nullable
-    public String getDigiZoomRatioDescription()
-    {
-        return getIndexedDescription(TAG_DIGI_ZOOM_RATIO, "Normal", null, "Digital 2x Zoom");
+    public String getDigitalZoomDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM);
+        if (value == null)
+            return null;
+        return value.toSimpleString(false);
+    }
+
+    @Nullable
+    public String getFocalPlaneDiagonalDescription()
+    {
+        Rational value = _directory.getRational(TAG_FOCAL_PLANE_DIAGONAL);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.###");
+        return format.format(value.doubleValue()) + " mm";
+    }
+
+    @Nullable
+    public String getCameraTypeDescription()
+    {
+        String cameratype = _directory.getString(TAG_CAMERA_TYPE);
+        if(cameratype == null)
+            return null;
+
+        if(OlympusMakernoteDirectory.OlympusCameraTypes.containsKey(cameratype))
+            return OlympusMakernoteDirectory.OlympusCameraTypes.get(cameratype);
+
+        return cameratype;
     }
 
@@ -680,4 +821,36 @@
 
     @Nullable
+    public String getOneTouchWbDescription()
+    {
+        return getIndexedDescription(TAG_ONE_TOUCH_WB, "Off", "On", "On (Preset)");
+    }
+
+    @Nullable
+    public String getShutterSpeedDescription()
+    {
+        return super.getShutterSpeedDescription(TAG_SHUTTER_SPEED_VALUE);
+    }
+
+    @Nullable
+    public String getIsoValueDescription()
+    {
+        Rational value = _directory.getRational(TAG_ISO_VALUE);
+        if (value == null)
+            return null;
+
+        return String.valueOf(Math.round(Math.pow(2, value.doubleValue() - 5) * 100));
+    }
+
+    @Nullable
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE_VALUE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return getFStopDescription(fStop);
+    }
+
+    @Nullable
     public String getMacroModeDescription()
     {
@@ -694,5 +867,54 @@
     public String getJpegQualityDescription()
     {
-        return getIndexedDescription(TAG_JPEG_QUALITY,
+        String cameratype = _directory.getString(TAG_CAMERA_TYPE);
+
+        if(cameratype != null)
+        {
+            Integer value = _directory.getInteger(TAG_JPEG_QUALITY);
+            if(value == null)
+                return null;
+
+            if((cameratype.startsWith("SX") && !cameratype.startsWith("SX151"))
+                || cameratype.startsWith("D4322"))
+            {
+                switch (value)
+                {
+                    case 0:
+                        return "Standard Quality (Low)";
+                    case 1:
+                        return "High Quality (Normal)";
+                    case 2:
+                        return "Super High Quality (Fine)";
+                    case 6:
+                        return "RAW";
+                    default:
+                        return "Unknown (" + value.toString() + ")";
+                }
+            }
+            else
+            {
+                switch (value)
+                {
+                    case 0:
+                        return "Standard Quality (Low)";
+                    case 1:
+                        return "High Quality (Normal)";
+                    case 2:
+                        return "Super High Quality (Fine)";
+                    case 4:
+                        return "RAW";
+                    case 5:
+                        return "Medium-Fine";
+                    case 6:
+                        return "Small-Fine";
+                    case 33:
+                        return "Uncompressed";
+                    default:
+                        return "Unknown (" + value.toString() + ")";
+                }
+            }
+        }
+        else
+            return getIndexedDescription(TAG_JPEG_QUALITY,
             1,
             "Standard Quality",
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,4 +34,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusMakernoteDirectory extends Directory
 {
@@ -117,8 +118,8 @@
 
     /** Zoom Factor (0 or 1 = normal) */
-    public static final int TAG_DIGI_ZOOM_RATIO = 0x0204;
+    public static final int TAG_DIGITAL_ZOOM = 0x0204;
     public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0205;
     public static final int TAG_LENS_DISTORTION_PARAMETERS = 0x0206;
-    public static final int TAG_FIRMWARE_VERSION = 0x0207;
+    public static final int TAG_CAMERA_TYPE = 0x0207;
     public static final int TAG_PICT_INFO = 0x0208;
     public static final int TAG_CAMERA_ID = 0x0209;
@@ -146,5 +147,6 @@
     public static final int TAG_WHITE_BALANCE_BIAS = 0x0304;
     public static final int TAG_SCENE_MODE = 0x0403;
-    public static final int TAG_FIRMWARE = 0x0404;
+    public static final int TAG_SERIAL_NUMBER_1 = 0x0404;
+    public static final int TAG_FIRMWARE = 0x0405;
 
     /**
@@ -176,23 +178,25 @@
     public static final int TAG_COLOUR_MATRIX = 0x1011;
     public static final int TAG_BLACK_LEVEL = 0x1012;
-//    public static final int TAG_ = 0x1013;
-//    public static final int TAG_ = 0x1014;
-    public static final int TAG_WHITE_BALANCE = 0x1015;
+    public static final int TAG_COLOR_TEMPERATURE_BG = 0x1013;
+    public static final int TAG_COLOR_TEMPERATURE_RG = 0x1014;
+    public static final int TAG_WB_MODE = 0x1015;
 //    public static final int TAG_ = 0x1016;
-    public static final int TAG_RED_BIAS = 0x1017;
-    public static final int TAG_BLUE_BIAS = 0x1018;
+    public static final int TAG_RED_BALANCE = 0x1017;
+    public static final int TAG_BLUE_BALANCE = 0x1018;
     public static final int TAG_COLOR_MATRIX_NUMBER = 0x1019;
-    public static final int TAG_SERIAL_NUMBER = 0x101A;
-//    public static final int TAG_ = 0x101B;
-//    public static final int TAG_ = 0x101C;
-//    public static final int TAG_ = 0x101D;
-//    public static final int TAG_ = 0x101E;
-//    public static final int TAG_ = 0x101F;
-//    public static final int TAG_ = 0x1020;
-//    public static final int TAG_ = 0x1021;
-//    public static final int TAG_ = 0x1022;
+    public static final int TAG_SERIAL_NUMBER_2 = 0x101A;
+
+    public static final int TAG_EXTERNAL_FLASH_AE1_0 = 0x101B;
+    public static final int TAG_EXTERNAL_FLASH_AE2_0 = 0x101C;
+    public static final int TAG_INTERNAL_FLASH_AE1_0 = 0x101D;
+    public static final int TAG_INTERNAL_FLASH_AE2_0 = 0x101E;
+    public static final int TAG_EXTERNAL_FLASH_AE1 = 0x101F;
+    public static final int TAG_EXTERNAL_FLASH_AE2 = 0x1020;
+    public static final int TAG_INTERNAL_FLASH_AE1 = 0x1021;
+    public static final int TAG_INTERNAL_FLASH_AE2 = 0x1022;
+
     public static final int TAG_FLASH_BIAS = 0x1023;
-//    public static final int TAG_ = 0x1024;
-//    public static final int TAG_ = 0x1025;
+    public static final int TAG_INTERNAL_FLASH_TABLE = 0x1024;
+    public static final int TAG_EXTERNAL_FLASH_G_VALUE = 0x1025;
     public static final int TAG_EXTERNAL_FLASH_BOUNCE = 0x1026;
     public static final int TAG_EXTERNAL_FLASH_ZOOM = 0x1027;
@@ -203,19 +207,22 @@
     public static final int TAG_VALID_BITS = 0x102C;
     public static final int TAG_CORING_FILTER = 0x102D;
-    public static final int TAG_FINAL_WIDTH = 0x102E;
-    public static final int TAG_FINAL_HEIGHT = 0x102F;
-//    public static final int TAG_ = 0x1030;
-//    public static final int TAG_ = 0x1031;
+    public static final int TAG_OLYMPUS_IMAGE_WIDTH = 0x102E;
+    public static final int TAG_OLYMPUS_IMAGE_HEIGHT = 0x102F;
+    public static final int TAG_SCENE_DETECT = 0x1030;
+    public static final int TAG_SCENE_AREA = 0x1031;
 //    public static final int TAG_ = 0x1032;
-//    public static final int TAG_ = 0x1033;
+    public static final int TAG_SCENE_DETECT_DATA = 0x1033;
     public static final int TAG_COMPRESSION_RATIO = 0x1034;
-    public static final int TAG_THUMBNAIL = 0x1035;
-    public static final int TAG_THUMBNAIL_OFFSET = 0x1036;
-    public static final int TAG_THUMBNAIL_LENGTH = 0x1037;
-//    public static final int TAG_ = 0x1038;
+    public static final int TAG_PREVIEW_IMAGE_VALID = 0x1035;
+    public static final int TAG_PREVIEW_IMAGE_START = 0x1036;
+    public static final int TAG_PREVIEW_IMAGE_LENGTH = 0x1037;
+    public static final int TAG_AF_RESULT = 0x1038;
     public static final int TAG_CCD_SCAN_MODE = 0x1039;
     public static final int TAG_NOISE_REDUCTION = 0x103A;
     public static final int TAG_INFINITY_LENS_STEP = 0x103B;
     public static final int TAG_NEAR_LENS_STEP = 0x103C;
+    public static final int TAG_LIGHT_VALUE_CENTER = 0x103D;
+    public static final int TAG_LIGHT_VALUE_PERIPHERY = 0x103E;
+    public static final int TAG_FIELD_COUNT = 0x103F;
     public static final int TAG_EQUIPMENT = 0x2010;
     public static final int TAG_CAMERA_SETTINGS = 0x2020;
@@ -225,4 +232,5 @@
     public static final int TAG_FOCUS_INFO = 0x2050;
     public static final int TAG_RAW_INFO = 0x3000;
+    public static final int TAG_MAIN_INFO = 0x4000;
 
     public final static class CameraSettings
@@ -302,8 +310,8 @@
         _tagNameMap.put(TAG_MACRO_MODE, "Macro");
         _tagNameMap.put(TAG_BW_MODE, "BW Mode");
-        _tagNameMap.put(TAG_DIGI_ZOOM_RATIO, "DigiZoom Ratio");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
         _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
         _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters");
-        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_CAMERA_TYPE, "Camera Type");
         _tagNameMap.put(TAG_PICT_INFO, "Pict Info");
         _tagNameMap.put(TAG_CAMERA_ID, "Camera Id");
@@ -318,4 +326,5 @@
         _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
         _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_SERIAL_NUMBER_1, "Serial Number");
         _tagNameMap.put(TAG_FIRMWARE, "Firmware");
         _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
@@ -341,10 +350,22 @@
         _tagNameMap.put(TAG_COLOUR_MATRIX, "Colour Matrix");
         _tagNameMap.put(TAG_BLACK_LEVEL, "Black Level");
-        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
-        _tagNameMap.put(TAG_RED_BIAS, "Red Bias");
-        _tagNameMap.put(TAG_BLUE_BIAS, "Blue Bias");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE_BG, "Color Temperature BG");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE_RG, "Color Temperature RG");
+        _tagNameMap.put(TAG_WB_MODE, "White Balance Mode");
+        _tagNameMap.put(TAG_RED_BALANCE, "Red Balance");
+        _tagNameMap.put(TAG_BLUE_BALANCE, "Blue Balance");
         _tagNameMap.put(TAG_COLOR_MATRIX_NUMBER, "Color Matrix Number");
-        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_SERIAL_NUMBER_2, "Serial Number");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE1_0, "External Flash AE1 0");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE2_0, "External Flash AE2 0");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE1_0, "Internal Flash AE1 0");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE2_0, "Internal Flash AE2 0");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE1, "External Flash AE1");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE2, "External Flash AE2");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE1, "Internal Flash AE1");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE2, "Internal Flash AE2");
         _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_TABLE, "Internal Flash Table");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_G_VALUE, "External Flash G Value");
         _tagNameMap.put(TAG_EXTERNAL_FLASH_BOUNCE, "External Flash Bounce");
         _tagNameMap.put(TAG_EXTERNAL_FLASH_ZOOM, "External Flash Zoom");
@@ -355,14 +376,21 @@
         _tagNameMap.put(TAG_VALID_BITS, "Valid Bits");
         _tagNameMap.put(TAG_CORING_FILTER, "Coring Filter");
-        _tagNameMap.put(TAG_FINAL_WIDTH, "Final Width");
-        _tagNameMap.put(TAG_FINAL_HEIGHT, "Final Height");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_WIDTH, "Olympus Image Width");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_HEIGHT, "Olympus Image Height");
+        _tagNameMap.put(TAG_SCENE_DETECT, "Scene Detect");
+        _tagNameMap.put(TAG_SCENE_AREA, "Scene Area");
+        _tagNameMap.put(TAG_SCENE_DETECT_DATA, "Scene Detect Data");
         _tagNameMap.put(TAG_COMPRESSION_RATIO, "Compression Ratio");
-        _tagNameMap.put(TAG_THUMBNAIL, "Thumbnail");
-        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
-        _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_VALID, "Preview Image Valid");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_START, "Preview Image Start");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_LENGTH, "Preview Image Length");
+        _tagNameMap.put(TAG_AF_RESULT, "AF Result");
         _tagNameMap.put(TAG_CCD_SCAN_MODE, "CCD Scan Mode");
         _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
         _tagNameMap.put(TAG_INFINITY_LENS_STEP, "Infinity Lens Step");
         _tagNameMap.put(TAG_NEAR_LENS_STEP, "Near Lens Step");
+        _tagNameMap.put(TAG_LIGHT_VALUE_CENTER, "Light Value Center");
+        _tagNameMap.put(TAG_LIGHT_VALUE_PERIPHERY, "Light Value Periphery");
+        _tagNameMap.put(TAG_FIELD_COUNT, "Field Count");
         _tagNameMap.put(TAG_EQUIPMENT, "Equipment");
         _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings");
@@ -372,4 +400,5 @@
         _tagNameMap.put(TAG_FOCUS_INFO, "Focus Info");
         _tagNameMap.put(TAG_RAW_INFO, "Raw Info");
+        _tagNameMap.put(TAG_MAIN_INFO, "Main Info");
 
         _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
@@ -476,3 +505,333 @@
         return _tagNameMap;
     }
+
+    // <summary>
+    // These values are currently decoded only for Olympus models.  Models with
+    // Olympus-style maker notes from other brands such as Acer, BenQ, Hitachi, HP,
+    // Premier, Konica-Minolta, Maginon, Ricoh, Rollei, SeaLife, Sony, Supra,
+    // Vivitar are not listed.
+    // </summary>
+    // <remarks>
+    // Converted from Exiftool version 10.33 created by Phil Harvey
+    // http://www.sno.phy.queensu.ca/~phil/exiftool/
+    // lib\Image\ExifTool\Olympus.pm
+    // </remarks>
+    public static final HashMap<String, String> OlympusCameraTypes = new HashMap<String, String>();
+
+    static {
+        OlympusCameraTypes.put("D4028", "X-2,C-50Z");
+        OlympusCameraTypes.put("D4029", "E-20,E-20N,E-20P");
+        OlympusCameraTypes.put("D4034", "C720UZ");
+        OlympusCameraTypes.put("D4040", "E-1");
+        OlympusCameraTypes.put("D4041", "E-300");
+        OlympusCameraTypes.put("D4083", "C2Z,D520Z,C220Z");
+        OlympusCameraTypes.put("D4106", "u20D,S400D,u400D");
+        OlympusCameraTypes.put("D4120", "X-1");
+        OlympusCameraTypes.put("D4122", "u10D,S300D,u300D");
+        OlympusCameraTypes.put("D4125", "AZ-1");
+        OlympusCameraTypes.put("D4141", "C150,D390");
+        OlympusCameraTypes.put("D4193", "C-5000Z");
+        OlympusCameraTypes.put("D4194", "X-3,C-60Z");
+        OlympusCameraTypes.put("D4199", "u30D,S410D,u410D");
+        OlympusCameraTypes.put("D4205", "X450,D535Z,C370Z");
+        OlympusCameraTypes.put("D4210", "C160,D395");
+        OlympusCameraTypes.put("D4211", "C725UZ");
+        OlympusCameraTypes.put("D4213", "FerrariMODEL2003");
+        OlympusCameraTypes.put("D4216", "u15D");
+        OlympusCameraTypes.put("D4217", "u25D");
+        OlympusCameraTypes.put("D4220", "u-miniD,Stylus V");
+        OlympusCameraTypes.put("D4221", "u40D,S500,uD500");
+        OlympusCameraTypes.put("D4231", "FerrariMODEL2004");
+        OlympusCameraTypes.put("D4240", "X500,D590Z,C470Z");
+        OlympusCameraTypes.put("D4244", "uD800,S800");
+        OlympusCameraTypes.put("D4256", "u720SW,S720SW");
+        OlympusCameraTypes.put("D4261", "X600,D630,FE5500");
+        OlympusCameraTypes.put("D4262", "uD600,S600");
+        OlympusCameraTypes.put("D4301", "u810/S810"); // (yes, "/".  Olympus is not consistent in the notation)
+        OlympusCameraTypes.put("D4302", "u710,S710");
+        OlympusCameraTypes.put("D4303", "u700,S700");
+        OlympusCameraTypes.put("D4304", "FE100,X710");
+        OlympusCameraTypes.put("D4305", "FE110,X705");
+        OlympusCameraTypes.put("D4310", "FE-130,X-720");
+        OlympusCameraTypes.put("D4311", "FE-140,X-725");
+        OlympusCameraTypes.put("D4312", "FE150,X730");
+        OlympusCameraTypes.put("D4313", "FE160,X735");
+        OlympusCameraTypes.put("D4314", "u740,S740");
+        OlympusCameraTypes.put("D4315", "u750,S750");
+        OlympusCameraTypes.put("D4316", "u730/S730");
+        OlympusCameraTypes.put("D4317", "FE115,X715");
+        OlympusCameraTypes.put("D4321", "SP550UZ");
+        OlympusCameraTypes.put("D4322", "SP510UZ");
+        OlympusCameraTypes.put("D4324", "FE170,X760");
+        OlympusCameraTypes.put("D4326", "FE200");
+        OlympusCameraTypes.put("D4327", "FE190/X750"); // (also SX876)
+        OlympusCameraTypes.put("D4328", "u760,S760");
+        OlympusCameraTypes.put("D4330", "FE180/X745"); // (also SX875)
+        OlympusCameraTypes.put("D4331", "u1000/S1000");
+        OlympusCameraTypes.put("D4332", "u770SW,S770SW");
+        OlympusCameraTypes.put("D4333", "FE240/X795");
+        OlympusCameraTypes.put("D4334", "FE210,X775");
+        OlympusCameraTypes.put("D4336", "FE230/X790");
+        OlympusCameraTypes.put("D4337", "FE220,X785");
+        OlympusCameraTypes.put("D4338", "u725SW,S725SW");
+        OlympusCameraTypes.put("D4339", "FE250/X800");
+        OlympusCameraTypes.put("D4341", "u780,S780");
+        OlympusCameraTypes.put("D4343", "u790SW,S790SW");
+        OlympusCameraTypes.put("D4344", "u1020,S1020");
+        OlympusCameraTypes.put("D4346", "FE15,X10");
+        OlympusCameraTypes.put("D4348", "FE280,X820,C520");
+        OlympusCameraTypes.put("D4349", "FE300,X830");
+        OlympusCameraTypes.put("D4350", "u820,S820");
+        OlympusCameraTypes.put("D4351", "u1200,S1200");
+        OlympusCameraTypes.put("D4352", "FE270,X815,C510");
+        OlympusCameraTypes.put("D4353", "u795SW,S795SW");
+        OlympusCameraTypes.put("D4354", "u1030SW,S1030SW");
+        OlympusCameraTypes.put("D4355", "SP560UZ");
+        OlympusCameraTypes.put("D4356", "u1010,S1010");
+        OlympusCameraTypes.put("D4357", "u830,S830");
+        OlympusCameraTypes.put("D4359", "u840,S840");
+        OlympusCameraTypes.put("D4360", "FE350WIDE,X865");
+        OlympusCameraTypes.put("D4361", "u850SW,S850SW");
+        OlympusCameraTypes.put("D4362", "FE340,X855,C560");
+        OlympusCameraTypes.put("D4363", "FE320,X835,C540");
+        OlympusCameraTypes.put("D4364", "SP570UZ");
+        OlympusCameraTypes.put("D4366", "FE330,X845,C550");
+        OlympusCameraTypes.put("D4368", "FE310,X840,C530");
+        OlympusCameraTypes.put("D4370", "u1050SW,S1050SW");
+        OlympusCameraTypes.put("D4371", "u1060,S1060");
+        OlympusCameraTypes.put("D4372", "FE370,X880,C575");
+        OlympusCameraTypes.put("D4374", "SP565UZ");
+        OlympusCameraTypes.put("D4377", "u1040,S1040");
+        OlympusCameraTypes.put("D4378", "FE360,X875,C570");
+        OlympusCameraTypes.put("D4379", "FE20,X15,C25");
+        OlympusCameraTypes.put("D4380", "uT6000,ST6000");
+        OlympusCameraTypes.put("D4381", "uT8000,ST8000");
+        OlympusCameraTypes.put("D4382", "u9000,S9000");
+        OlympusCameraTypes.put("D4384", "SP590UZ");
+        OlympusCameraTypes.put("D4385", "FE3010,X895");
+        OlympusCameraTypes.put("D4386", "FE3000,X890");
+        OlympusCameraTypes.put("D4387", "FE35,X30");
+        OlympusCameraTypes.put("D4388", "u550WP,S550WP");
+        OlympusCameraTypes.put("D4390", "FE5000,X905");
+        OlympusCameraTypes.put("D4391", "u5000");
+        OlympusCameraTypes.put("D4392", "u7000,S7000");
+        OlympusCameraTypes.put("D4396", "FE5010,X915");
+        OlympusCameraTypes.put("D4397", "FE25,X20");
+        OlympusCameraTypes.put("D4398", "FE45,X40");
+        OlympusCameraTypes.put("D4401", "XZ-1");
+        OlympusCameraTypes.put("D4402", "uT6010,ST6010");
+        OlympusCameraTypes.put("D4406", "u7010,S7010 / u7020,S7020");
+        OlympusCameraTypes.put("D4407", "FE4010,X930");
+        OlympusCameraTypes.put("D4408", "X560WP");
+        OlympusCameraTypes.put("D4409", "FE26,X21");
+        OlympusCameraTypes.put("D4410", "FE4000,X920,X925");
+        OlympusCameraTypes.put("D4411", "FE46,X41,X42");
+        OlympusCameraTypes.put("D4412", "FE5020,X935");
+        OlympusCameraTypes.put("D4413", "uTough-3000");
+        OlympusCameraTypes.put("D4414", "StylusTough-6020");
+        OlympusCameraTypes.put("D4415", "StylusTough-8010");
+        OlympusCameraTypes.put("D4417", "u5010,S5010");
+        OlympusCameraTypes.put("D4418", "u7040,S7040");
+        OlympusCameraTypes.put("D4419", "u9010,S9010");
+        OlympusCameraTypes.put("D4423", "FE4040");
+        OlympusCameraTypes.put("D4424", "FE47,X43");
+        OlympusCameraTypes.put("D4426", "FE4030,X950");
+        OlympusCameraTypes.put("D4428", "FE5030,X965,X960");
+        OlympusCameraTypes.put("D4430", "u7030,S7030");
+        OlympusCameraTypes.put("D4432", "SP600UZ");
+        OlympusCameraTypes.put("D4434", "SP800UZ");
+        OlympusCameraTypes.put("D4439", "FE4020,X940");
+        OlympusCameraTypes.put("D4442", "FE5035");
+        OlympusCameraTypes.put("D4448", "FE4050,X970");
+        OlympusCameraTypes.put("D4450", "FE5050,X985");
+        OlympusCameraTypes.put("D4454", "u-7050");
+        OlympusCameraTypes.put("D4464", "T10,X27");
+        OlympusCameraTypes.put("D4470", "FE5040,X980");
+        OlympusCameraTypes.put("D4472", "TG-310");
+        OlympusCameraTypes.put("D4474", "TG-610");
+        OlympusCameraTypes.put("D4476", "TG-810");
+        OlympusCameraTypes.put("D4478", "VG145,VG140,D715");
+        OlympusCameraTypes.put("D4479", "VG130,D710");
+        OlympusCameraTypes.put("D4480", "VG120,D705");
+        OlympusCameraTypes.put("D4482", "VR310,D720");
+        OlympusCameraTypes.put("D4484", "VR320,D725");
+        OlympusCameraTypes.put("D4486", "VR330,D730");
+        OlympusCameraTypes.put("D4488", "VG110,D700");
+        OlympusCameraTypes.put("D4490", "SP-610UZ");
+        OlympusCameraTypes.put("D4492", "SZ-10");
+        OlympusCameraTypes.put("D4494", "SZ-20");
+        OlympusCameraTypes.put("D4496", "SZ-30MR");
+        OlympusCameraTypes.put("D4498", "SP-810UZ");
+        OlympusCameraTypes.put("D4500", "SZ-11");
+        OlympusCameraTypes.put("D4504", "TG-615");
+        OlympusCameraTypes.put("D4508", "TG-620");
+        OlympusCameraTypes.put("D4510", "TG-820");
+        OlympusCameraTypes.put("D4512", "TG-1");
+        OlympusCameraTypes.put("D4516", "SH-21");
+        OlympusCameraTypes.put("D4519", "SZ-14");
+        OlympusCameraTypes.put("D4520", "SZ-31MR");
+        OlympusCameraTypes.put("D4521", "SH-25MR");
+        OlympusCameraTypes.put("D4523", "SP-720UZ");
+        OlympusCameraTypes.put("D4529", "VG170");
+        OlympusCameraTypes.put("D4531", "XZ-2");
+        OlympusCameraTypes.put("D4535", "SP-620UZ");
+        OlympusCameraTypes.put("D4536", "TG-320");
+        OlympusCameraTypes.put("D4537", "VR340,D750");
+        OlympusCameraTypes.put("D4538", "VG160,X990,D745");
+        OlympusCameraTypes.put("D4541", "SZ-12");
+        OlympusCameraTypes.put("D4545", "VH410");
+        OlympusCameraTypes.put("D4546", "XZ-10"); //IB
+        OlympusCameraTypes.put("D4547", "TG-2");
+        OlympusCameraTypes.put("D4548", "TG-830");
+        OlympusCameraTypes.put("D4549", "TG-630");
+        OlympusCameraTypes.put("D4550", "SH-50");
+        OlympusCameraTypes.put("D4553", "SZ-16,DZ-105");
+        OlympusCameraTypes.put("D4562", "SP-820UZ");
+        OlympusCameraTypes.put("D4566", "SZ-15");
+        OlympusCameraTypes.put("D4572", "STYLUS1");
+        OlympusCameraTypes.put("D4574", "TG-3");
+        OlympusCameraTypes.put("D4575", "TG-850");
+        OlympusCameraTypes.put("D4579", "SP-100EE");
+        OlympusCameraTypes.put("D4580", "SH-60");
+        OlympusCameraTypes.put("D4581", "SH-1");
+        OlympusCameraTypes.put("D4582", "TG-835");
+        OlympusCameraTypes.put("D4585", "SH-2 / SH-3");
+        OlympusCameraTypes.put("D4586", "TG-4");
+        OlympusCameraTypes.put("D4587", "TG-860");
+        OlympusCameraTypes.put("D4591", "TG-870");
+        OlympusCameraTypes.put("D4809", "C2500L");
+        OlympusCameraTypes.put("D4842", "E-10");
+        OlympusCameraTypes.put("D4856", "C-1");
+        OlympusCameraTypes.put("D4857", "C-1Z,D-150Z");
+        OlympusCameraTypes.put("DCHC", "D500L");
+        OlympusCameraTypes.put("DCHT", "D600L / D620L");
+        OlympusCameraTypes.put("K0055", "AIR-A01");
+        OlympusCameraTypes.put("S0003", "E-330");
+        OlympusCameraTypes.put("S0004", "E-500");
+        OlympusCameraTypes.put("S0009", "E-400");
+        OlympusCameraTypes.put("S0010", "E-510");
+        OlympusCameraTypes.put("S0011", "E-3");
+        OlympusCameraTypes.put("S0013", "E-410");
+        OlympusCameraTypes.put("S0016", "E-420");
+        OlympusCameraTypes.put("S0017", "E-30");
+        OlympusCameraTypes.put("S0018", "E-520");
+        OlympusCameraTypes.put("S0019", "E-P1");
+        OlympusCameraTypes.put("S0023", "E-620");
+        OlympusCameraTypes.put("S0026", "E-P2");
+        OlympusCameraTypes.put("S0027", "E-PL1");
+        OlympusCameraTypes.put("S0029", "E-450");
+        OlympusCameraTypes.put("S0030", "E-600");
+        OlympusCameraTypes.put("S0032", "E-P3");
+        OlympusCameraTypes.put("S0033", "E-5");
+        OlympusCameraTypes.put("S0034", "E-PL2");
+        OlympusCameraTypes.put("S0036", "E-M5");
+        OlympusCameraTypes.put("S0038", "E-PL3");
+        OlympusCameraTypes.put("S0039", "E-PM1");
+        OlympusCameraTypes.put("S0040", "E-PL1s");
+        OlympusCameraTypes.put("S0042", "E-PL5");
+        OlympusCameraTypes.put("S0043", "E-PM2");
+        OlympusCameraTypes.put("S0044", "E-P5");
+        OlympusCameraTypes.put("S0045", "E-PL6");
+        OlympusCameraTypes.put("S0046", "E-PL7"); //IB
+        OlympusCameraTypes.put("S0047", "E-M1");
+        OlympusCameraTypes.put("S0051", "E-M10");
+        OlympusCameraTypes.put("S0052", "E-M5MarkII"); //IB
+        OlympusCameraTypes.put("S0059", "E-M10MarkII");
+        OlympusCameraTypes.put("S0061", "PEN-F"); //forum7005
+        OlympusCameraTypes.put("S0065", "E-PL8");
+        OlympusCameraTypes.put("S0067", "E-M1MarkII");
+        OlympusCameraTypes.put("SR45", "D220");
+        OlympusCameraTypes.put("SR55", "D320L");
+        OlympusCameraTypes.put("SR83", "D340L");
+        OlympusCameraTypes.put("SR85", "C830L,D340R");
+        OlympusCameraTypes.put("SR852", "C860L,D360L");
+        OlympusCameraTypes.put("SR872", "C900Z,D400Z");
+        OlympusCameraTypes.put("SR874", "C960Z,D460Z");
+        OlympusCameraTypes.put("SR951", "C2000Z");
+        OlympusCameraTypes.put("SR952", "C21");
+        OlympusCameraTypes.put("SR953", "C21T.commu");
+        OlympusCameraTypes.put("SR954", "C2020Z");
+        OlympusCameraTypes.put("SR955", "C990Z,D490Z");
+        OlympusCameraTypes.put("SR956", "C211Z");
+        OlympusCameraTypes.put("SR959", "C990ZS,D490Z");
+        OlympusCameraTypes.put("SR95A", "C2100UZ");
+        OlympusCameraTypes.put("SR971", "C100,D370");
+        OlympusCameraTypes.put("SR973", "C2,D230");
+        OlympusCameraTypes.put("SX151", "E100RS");
+        OlympusCameraTypes.put("SX351", "C3000Z / C3030Z");
+        OlympusCameraTypes.put("SX354", "C3040Z");
+        OlympusCameraTypes.put("SX355", "C2040Z");
+        OlympusCameraTypes.put("SX357", "C700UZ");
+        OlympusCameraTypes.put("SX358", "C200Z,D510Z");
+        OlympusCameraTypes.put("SX374", "C3100Z,C3020Z");
+        OlympusCameraTypes.put("SX552", "C4040Z");
+        OlympusCameraTypes.put("SX553", "C40Z,D40Z");
+        OlympusCameraTypes.put("SX556", "C730UZ");
+        OlympusCameraTypes.put("SX558", "C5050Z");
+        OlympusCameraTypes.put("SX571", "C120,D380");
+        OlympusCameraTypes.put("SX574", "C300Z,D550Z");
+        OlympusCameraTypes.put("SX575", "C4100Z,C4000Z");
+        OlympusCameraTypes.put("SX751", "X200,D560Z,C350Z");
+        OlympusCameraTypes.put("SX752", "X300,D565Z,C450Z");
+        OlympusCameraTypes.put("SX753", "C750UZ");
+        OlympusCameraTypes.put("SX754", "C740UZ");
+        OlympusCameraTypes.put("SX755", "C755UZ");
+        OlympusCameraTypes.put("SX756", "C5060WZ");
+        OlympusCameraTypes.put("SX757", "C8080WZ");
+        OlympusCameraTypes.put("SX758", "X350,D575Z,C360Z");
+        OlympusCameraTypes.put("SX759", "X400,D580Z,C460Z");
+        OlympusCameraTypes.put("SX75A", "AZ-2ZOOM");
+        OlympusCameraTypes.put("SX75B", "D595Z,C500Z");
+        OlympusCameraTypes.put("SX75C", "X550,D545Z,C480Z");
+        OlympusCameraTypes.put("SX75D", "IR-300");
+        OlympusCameraTypes.put("SX75F", "C55Z,C5500Z");
+        OlympusCameraTypes.put("SX75G", "C170,D425");
+        OlympusCameraTypes.put("SX75J", "C180,D435");
+        OlympusCameraTypes.put("SX771", "C760UZ");
+        OlympusCameraTypes.put("SX772", "C770UZ");
+        OlympusCameraTypes.put("SX773", "C745UZ");
+        OlympusCameraTypes.put("SX774", "X250,D560Z,C350Z");
+        OlympusCameraTypes.put("SX775", "X100,D540Z,C310Z");
+        OlympusCameraTypes.put("SX776", "C460ZdelSol");
+        OlympusCameraTypes.put("SX777", "C765UZ");
+        OlympusCameraTypes.put("SX77A", "D555Z,C315Z");
+        OlympusCameraTypes.put("SX851", "C7070WZ");
+        OlympusCameraTypes.put("SX852", "C70Z,C7000Z");
+        OlympusCameraTypes.put("SX853", "SP500UZ");
+        OlympusCameraTypes.put("SX854", "SP310");
+        OlympusCameraTypes.put("SX855", "SP350");
+        OlympusCameraTypes.put("SX873", "SP320");
+        OlympusCameraTypes.put("SX875", "FE180/X745"); // (also D4330)
+        OlympusCameraTypes.put("SX876", "FE190/X750"); // (also D4327)
+
+        //   other brands
+        //    4MP9Q3", "Camera 4MP-9Q3'
+        //    4MP9T2", "BenQ DC C420 / Camera 4MP-9T2'
+        //    5MP9Q3", "Camera 5MP-9Q3" },
+        //    5MP9X9", "Camera 5MP-9X9" },
+        //   '5MP-9T'=> 'Camera 5MP-9T3" },
+        //   '5MP-9Y'=> 'Camera 5MP-9Y2" },
+        //   '6MP-9U'=> 'Camera 6MP-9U9" },
+        //    7MP9Q3", "Camera 7MP-9Q3" },
+        //   '8MP-9U'=> 'Camera 8MP-9U4" },
+        //    CE5330", "Acer CE-5330" },
+        //   'CP-853'=> 'Acer CP-8531" },
+        //    CS5531", "Acer CS5531" },
+        //    DC500 ", "SeaLife DC500" },
+        //    DC7370", "Camera 7MP-9GA" },
+        //    DC7371", "Camera 7MP-9GM" },
+        //    DC7371", "Hitachi HDC-751E" },
+        //    DC7375", "Hitachi HDC-763E / Rollei RCP-7330X / Ricoh Caplio RR770 / Vivitar ViviCam 7330" },
+        //   'DC E63'=> 'BenQ DC E63+" },
+        //   'DC P86'=> 'BenQ DC P860" },
+        //    DS5340", "Maginon Performic S5 / Premier 5MP-9M7" },
+        //    DS5341", "BenQ E53+ / Supra TCM X50 / Maginon X50 / Premier 5MP-9P8" },
+        //    DS5346", "Premier 5MP-9Q2" },
+        //    E500  ", "Konica Minolta DiMAGE E500" },
+        //    MAGINO", "Maginon X60" },
+        //    Mz60  ", "HP Photosmart Mz60" },
+        //    Q3DIGI", "Camera 5MP-9Q3" },
+        //    SLIMLI", "Supra Slimline X6" },
+        //    V8300s", "Vivitar V8300s" },
+    }
 }
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.util.HashMap;
+
+import static com.drew.metadata.exif.makernotes.OlympusRawDevelopment2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusRawDevelopment2MakernoteDirectory}.
+ * <p>
+ * Some Description functions 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopment2MakernoteDescriptor extends TagDescriptor<OlympusRawDevelopment2MakernoteDirectory>
+{
+    public OlympusRawDevelopment2MakernoteDescriptor(@NotNull OlympusRawDevelopment2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagRawDevVersion:
+                return getRawDevVersionDescription();
+            case TagRawDevExposureBiasValue:
+                return getRawDevExposureBiasValueDescription();
+            case TagRawDevColorSpace:
+                return getRawDevColorSpaceDescription();
+            case TagRawDevNoiseReduction:
+                return getRawDevNoiseReductionDescription();
+            case TagRawDevEngine:
+                return getRawDevEngineDescription();
+            case TagRawDevPictureMode:
+                return getRawDevPictureModeDescription();
+            case TagRawDevPmBwFilter:
+                return getRawDevPmBwFilterDescription();
+            case TagRawDevPmPictureTone:
+                return getRawDevPmPictureToneDescription();
+            case TagRawDevArtFilter:
+                return getRawDevArtFilterDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getRawDevVersionDescription()
+    {
+        return getVersionBytesDescription(TagRawDevVersion, 4);
+    }
+
+    @Nullable
+    public String getRawDevExposureBiasValueDescription()
+    {
+        return getIndexedDescription(TagRawDevExposureBiasValue,
+                1, "Color Temperature", "Gray Point");
+    }
+
+    @Nullable
+    public String getRawDevColorSpaceDescription()
+    {
+        return getIndexedDescription(TagRawDevColorSpace,
+            "sRGB", "Adobe RGB", "Pro Photo RGB");
+    }
+
+    @Nullable
+    public String getRawDevNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevNoiseReduction);
+        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), ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getRawDevEngineDescription()
+    {
+        return getIndexedDescription(TagRawDevEngine,
+            "High Speed", "High Function", "Advanced High Speed", "Advanced High Function");
+    }
+
+    @Nullable
+    public String getRawDevPictureModeDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevPictureMode);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 1:
+                return "Vivid";
+            case 2:
+                return "Natural";
+            case 3:
+                return "Muted";
+            case 256:
+                return "Monotone";
+            case 512:
+                return "Sepia";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getRawDevPmBwFilterDescription()
+    {
+        return getIndexedDescription(TagRawDevPmBwFilter,
+            "Neutral", "Yellow", "Orange", "Red", "Green");
+    }
+
+    @Nullable
+    public String getRawDevPmPictureToneDescription()
+    {
+        return getIndexedDescription(TagRawDevPmPictureTone,
+            "Neutral", "Sepia", "Blue", "Purple", "Green");
+    }
+
+    @Nullable
+    public String getRawDevArtFilterDescription()
+    {
+        return getFilterDescription(TagRawDevArtFilter);
+    }
+
+    @Nullable
+    public String getFilterDescription(int tag)
+    {
+        int[] values = _directory.getIntArray(tag);
+        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]).append("; ");
+            sb.append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    // RawDevArtFilter 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/OlympusRawDevelopment2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,111 @@
+/*
+ * 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 raw development 2 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopment2MakernoteDirectory extends Directory
+{    
+    public static final int TagRawDevVersion = 0x0000;
+    public static final int TagRawDevExposureBiasValue = 0x0100;
+    public static final int TagRawDevWhiteBalance = 0x0101;
+    public static final int TagRawDevWhiteBalanceValue = 0x0102;
+    public static final int TagRawDevWbFineAdjustment = 0x0103;
+    public static final int TagRawDevGrayPoint = 0x0104;
+    public static final int TagRawDevContrastValue = 0x0105;
+    public static final int TagRawDevSharpnessValue = 0x0106;
+    public static final int TagRawDevSaturationEmphasis = 0x0107;
+    public static final int TagRawDevMemoryColorEmphasis = 0x0108;
+    public static final int TagRawDevColorSpace = 0x0109;
+    public static final int TagRawDevNoiseReduction = 0x010a;
+    public static final int TagRawDevEngine = 0x010b;
+    public static final int TagRawDevPictureMode = 0x010c;
+    public static final int TagRawDevPmSaturation = 0x010d;
+    public static final int TagRawDevPmContrast = 0x010e;
+    public static final int TagRawDevPmSharpness = 0x010f;
+    public static final int TagRawDevPmBwFilter = 0x0110;
+    public static final int TagRawDevPmPictureTone = 0x0111;
+    public static final int TagRawDevGradation = 0x0112;
+    public static final int TagRawDevSaturation3 = 0x0113;
+    public static final int TagRawDevAutoGradation = 0x0119;
+    public static final int TagRawDevPmNoiseFilter = 0x0120;
+    public static final int TagRawDevArtFilter = 0x0121;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {        
+        _tagNameMap.put(TagRawDevVersion, "Raw Dev Version");
+        _tagNameMap.put(TagRawDevExposureBiasValue, "Raw Dev Exposure Bias Value");
+        _tagNameMap.put(TagRawDevWhiteBalance, "Raw Dev White Balance");
+        _tagNameMap.put(TagRawDevWhiteBalanceValue, "Raw Dev White Balance Value");
+        _tagNameMap.put(TagRawDevWbFineAdjustment, "Raw Dev WB Fine Adjustment");
+        _tagNameMap.put(TagRawDevGrayPoint, "Raw Dev Gray Point");
+        _tagNameMap.put(TagRawDevContrastValue, "Raw Dev Contrast Value");
+        _tagNameMap.put(TagRawDevSharpnessValue, "Raw Dev Sharpness Value");
+        _tagNameMap.put(TagRawDevSaturationEmphasis, "Raw Dev Saturation Emphasis");
+        _tagNameMap.put(TagRawDevMemoryColorEmphasis, "Raw Dev Memory Color Emphasis");
+        _tagNameMap.put(TagRawDevColorSpace, "Raw Dev Color Space");
+        _tagNameMap.put(TagRawDevNoiseReduction, "Raw Dev Noise Reduction");
+        _tagNameMap.put(TagRawDevEngine, "Raw Dev Engine");
+        _tagNameMap.put(TagRawDevPictureMode, "Raw Dev Picture Mode");
+        _tagNameMap.put(TagRawDevPmSaturation, "Raw Dev PM Saturation");
+        _tagNameMap.put(TagRawDevPmContrast, "Raw Dev PM Contrast");
+        _tagNameMap.put(TagRawDevPmSharpness, "Raw Dev PM Sharpness");
+        _tagNameMap.put(TagRawDevPmBwFilter, "Raw Dev PM BW Filter");
+        _tagNameMap.put(TagRawDevPmPictureTone, "Raw Dev PM Picture Tone");
+        _tagNameMap.put(TagRawDevGradation, "Raw Dev Gradation");
+        _tagNameMap.put(TagRawDevSaturation3, "Raw Dev Saturation 3");
+        _tagNameMap.put(TagRawDevAutoGradation, "Raw Dev Auto Gradation");
+        _tagNameMap.put(TagRawDevPmNoiseFilter, "Raw Dev PM Noise Filter");
+        _tagNameMap.put(TagRawDevArtFilter, "Raw Dev Art Filter");
+    }
+
+    public OlympusRawDevelopment2MakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusRawDevelopment2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Raw Development 2";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusRawDevelopmentMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusRawDevelopmentMakernoteDirectory}.
+ * <p>
+ * Some Description functions 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopmentMakernoteDescriptor extends TagDescriptor<OlympusRawDevelopmentMakernoteDirectory>
+{
+    public OlympusRawDevelopmentMakernoteDescriptor(@NotNull OlympusRawDevelopmentMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagRawDevVersion:
+                return getRawDevVersionDescription();
+            case TagRawDevColorSpace:
+                return getRawDevColorSpaceDescription();
+            case TagRawDevEngine:
+                return getRawDevEngineDescription();
+            case TagRawDevNoiseReduction:
+                return getRawDevNoiseReductionDescription();
+            case TagRawDevEditStatus:
+                return getRawDevEditStatusDescription();
+            case TagRawDevSettings:
+                return getRawDevSettingsDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getRawDevVersionDescription()
+    {
+        return getVersionBytesDescription(TagRawDevVersion, 4);
+    }
+
+    @Nullable
+    public String getRawDevColorSpaceDescription()
+    {
+        return getIndexedDescription(TagRawDevColorSpace,
+            "sRGB", "Adobe RGB", "Pro Photo RGB");
+    }
+
+    @Nullable
+    public String getRawDevEngineDescription()
+    {
+        return getIndexedDescription(TagRawDevEngine,
+            "High Speed", "High Function", "Advanced High Speed", "Advanced High Function");
+    }
+
+    @Nullable
+    public String getRawDevNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevNoiseReduction);
+        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), ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getRawDevEditStatusDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevEditStatus);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "Original";
+            case 1:
+                return "Edited (Landscape)";
+            case 6:
+            case 8:
+                return "Edited (Portrait)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getRawDevSettingsDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevSettings);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if ((v        & 1) != 0) sb.append("WB Color Temp, ");
+        if (((v >> 1) & 1) != 0) sb.append("WB Gray Point, ");
+        if (((v >> 2) & 1) != 0) sb.append("Saturation, ");
+        if (((v >> 3) & 1) != 0) sb.append("Contrast, ");
+        if (((v >> 4) & 1) != 0) sb.append("Sharpness, ");
+        if (((v >> 5) & 1) != 0) sb.append("Color Space, ");
+        if (((v >> 6) & 1) != 0) sb.append("High Function, ");
+        if (((v >> 7) & 1) != 0) sb.append("Noise Reduction, ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,91 @@
+/*
+ * 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 raw development 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopmentMakernoteDirectory extends Directory
+{
+    public static final int TagRawDevVersion = 0x0000;
+    public static final int TagRawDevExposureBiasValue = 0x0100;
+    public static final int TagRawDevWhiteBalanceValue = 0x0101;
+    public static final int TagRawDevWbFineAdjustment = 0x0102;
+    public static final int TagRawDevGrayPoint = 0x0103;
+    public static final int TagRawDevSaturationEmphasis = 0x0104;
+    public static final int TagRawDevMemoryColorEmphasis = 0x0105;
+    public static final int TagRawDevContrastValue = 0x0106;
+    public static final int TagRawDevSharpnessValue = 0x0107;
+    public static final int TagRawDevColorSpace = 0x0108;
+    public static final int TagRawDevEngine = 0x0109;
+    public static final int TagRawDevNoiseReduction = 0x010a;
+    public static final int TagRawDevEditStatus = 0x010b;
+    public static final int TagRawDevSettings = 0x010c;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagRawDevVersion, "Raw Dev Version");
+        _tagNameMap.put(TagRawDevExposureBiasValue, "Raw Dev Exposure Bias Value");
+        _tagNameMap.put(TagRawDevWhiteBalanceValue, "Raw Dev White Balance Value");
+        _tagNameMap.put(TagRawDevWbFineAdjustment, "Raw Dev WB Fine Adjustment");
+        _tagNameMap.put(TagRawDevGrayPoint, "Raw Dev Gray Point");
+        _tagNameMap.put(TagRawDevSaturationEmphasis, "Raw Dev Saturation Emphasis");
+        _tagNameMap.put(TagRawDevMemoryColorEmphasis, "Raw Dev Memory Color Emphasis");
+        _tagNameMap.put(TagRawDevContrastValue, "Raw Dev Contrast Value");
+        _tagNameMap.put(TagRawDevSharpnessValue, "Raw Dev Sharpness Value");
+        _tagNameMap.put(TagRawDevColorSpace, "Raw Dev Color Space");
+        _tagNameMap.put(TagRawDevEngine, "Raw Dev Engine");
+        _tagNameMap.put(TagRawDevNoiseReduction, "Raw Dev Noise Reduction");
+        _tagNameMap.put(TagRawDevEditStatus, "Raw Dev Edit Status");
+        _tagNameMap.put(TagRawDevSettings, "Raw Dev Settings");
+    }
+
+    public OlympusRawDevelopmentMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusRawDevelopmentMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Raw Development";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusRawInfoMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusRawInfoMakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.33 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
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawInfoMakernoteDescriptor extends TagDescriptor<OlympusRawInfoMakernoteDirectory>
+{
+    public OlympusRawInfoMakernoteDescriptor(@NotNull OlympusRawInfoMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagRawInfoVersion:
+                return getVersionBytesDescription(TagRawInfoVersion, 4);
+            case TagColorMatrix2:
+                return getColorMatrix2Description();
+            case TagYCbCrCoefficients:
+                return getYCbCrCoefficientsDescription();
+            case TagLightSource:
+                return getOlympusLightSourceDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getColorMatrix2Description()
+    {
+        int[] values = _directory.getIntArray(TagColorMatrix2);
+        if (values == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            sb.append((short)values[i]);
+            if (i < values.length - 1)
+                sb.append(" ");
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    @Nullable
+    public String getYCbCrCoefficientsDescription()
+    {
+        int[] values = _directory.getIntArray(TagYCbCrCoefficients);
+        if (values == null)
+            return null;
+
+        Rational[] ret = new Rational[values.length / 2];
+        for(int i = 0; i < values.length / 2; i++)
+        {
+            ret[i] = new Rational((short)values[2*i], (short)values[2*i + 1]);
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < ret.length; i++) {
+            sb.append(ret[i].doubleValue());
+            if (i < ret.length - 1)
+                sb.append(" ");
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+    
+    @Nullable
+    public String getOlympusLightSourceDescription()
+    {
+        Integer value = _directory.getInteger(TagLightSource);
+        if (value == null)
+            return null;
+
+        switch (value.shortValue())
+        {
+            case 0:
+                return "Unknown";
+            case 16:
+                return "Shade";
+            case 17:
+                return "Cloudy";
+            case 18:
+                return "Fine Weather";
+            case 20:
+                return "Tungsten (Incandescent)";
+            case 22:
+                return "Evening Sunlight";
+            case 33:
+                return "Daylight Fluorescent";
+            case 34:
+                return "Day White Fluorescent";
+            case 35:
+                return "Cool White Fluorescent";
+            case 36:
+                return "White Fluorescent";
+            case 256:
+                return "One Touch White Balance";
+            case 512:
+                return "Custom 1-4";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+/**
+ * These tags are found only in ORF images of some models (eg. C8080WZ)
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawInfoMakernoteDirectory extends Directory
+{
+    public static final int TagRawInfoVersion = 0x0000;
+    public static final int TagWbRbLevelsUsed = 0x0100;
+    public static final int TagWbRbLevelsAuto = 0x0110;
+    public static final int TagWbRbLevelsShade = 0x0120;
+    public static final int TagWbRbLevelsCloudy = 0x0121;
+    public static final int TagWbRbLevelsFineWeather = 0x0122;
+    public static final int TagWbRbLevelsTungsten = 0x0123;
+    public static final int TagWbRbLevelsEveningSunlight = 0x0124;
+    public static final int TagWbRbLevelsDaylightFluor = 0x0130;
+    public static final int TagWbRbLevelsDayWhiteFluor = 0x0131;
+    public static final int TagWbRbLevelsCoolWhiteFluor = 0x0132;
+    public static final int TagWbRbLevelsWhiteFluorescent = 0x0133;
+
+    public static final int TagColorMatrix2 = 0x0200;
+    public static final int TagCoringFilter = 0x0310;
+    public static final int TagCoringValues = 0x0311;
+    public static final int TagBlackLevel2 = 0x0600;
+    public static final int TagYCbCrCoefficients = 0x0601;
+    public static final int TagValidPixelDepth = 0x0611;
+    public static final int TagCropLeft = 0x0612;
+    public static final int TagCropTop = 0x0613;
+    public static final int TagCropWidth = 0x0614;
+    public static final int TagCropHeight = 0x0615;
+
+    public static final int TagLightSource = 0x1000;
+
+    //the following 5 tags all have 3 values: val, min, max
+    public static final int TagWhiteBalanceComp = 0x1001;
+    public static final int TagSaturationSetting = 0x1010;
+    public static final int TagHueSetting = 0x1011;
+    public static final int TagContrastSetting = 0x1012;
+    public static final int TagSharpnessSetting = 0x1013;
+
+    // settings written by Camedia Master 4.x
+    public static final int TagCmExposureCompensation = 0x2000;
+    public static final int TagCmWhiteBalance = 0x2001;
+    public static final int TagCmWhiteBalanceComp = 0x2002;
+    public static final int TagCmWhiteBalanceGrayPoint = 0x2010;
+    public static final int TagCmSaturation = 0x2020;
+    public static final int TagCmHue = 0x2021;
+    public static final int TagCmContrast = 0x2022;
+    public static final int TagCmSharpness = 0x2023;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagRawInfoVersion, "Raw Info Version");
+        _tagNameMap.put(TagWbRbLevelsUsed, "WB RB Levels Used");
+        _tagNameMap.put(TagWbRbLevelsAuto, "WB RB Levels Auto");
+        _tagNameMap.put(TagWbRbLevelsShade, "WB RB Levels Shade");
+        _tagNameMap.put(TagWbRbLevelsCloudy, "WB RB Levels Cloudy");
+        _tagNameMap.put(TagWbRbLevelsFineWeather, "WB RB Levels Fine Weather");
+        _tagNameMap.put(TagWbRbLevelsTungsten, "WB RB Levels Tungsten");
+        _tagNameMap.put(TagWbRbLevelsEveningSunlight, "WB RB Levels Evening Sunlight");
+        _tagNameMap.put(TagWbRbLevelsDaylightFluor, "WB RB Levels Daylight Fluor");
+        _tagNameMap.put(TagWbRbLevelsDayWhiteFluor, "WB RB Levels Day White Fluor");
+        _tagNameMap.put(TagWbRbLevelsCoolWhiteFluor, "WB RB Levels Cool White Fluor");
+        _tagNameMap.put(TagWbRbLevelsWhiteFluorescent, "WB RB Levels White Fluorescent");
+        _tagNameMap.put(TagColorMatrix2, "Color Matrix 2");
+        _tagNameMap.put(TagCoringFilter, "Coring Filter");
+        _tagNameMap.put(TagCoringValues, "Coring Values");
+        _tagNameMap.put(TagBlackLevel2, "Black Level 2");
+        _tagNameMap.put(TagYCbCrCoefficients, "YCbCrCoefficients");
+        _tagNameMap.put(TagValidPixelDepth, "Valid Pixel Depth");
+        _tagNameMap.put(TagCropLeft, "Crop Left");
+        _tagNameMap.put(TagCropTop, "Crop Top");
+        _tagNameMap.put(TagCropWidth, "Crop Width");
+        _tagNameMap.put(TagCropHeight, "Crop Height");
+        _tagNameMap.put(TagLightSource, "Light Source");
+
+        _tagNameMap.put(TagWhiteBalanceComp, "White Balance Comp");
+        _tagNameMap.put(TagSaturationSetting, "Saturation Setting");
+        _tagNameMap.put(TagHueSetting, "Hue Setting");
+        _tagNameMap.put(TagContrastSetting, "Contrast Setting");
+        _tagNameMap.put(TagSharpnessSetting, "Sharpness Setting");
+
+        _tagNameMap.put(TagCmExposureCompensation, "CM Exposure Compensation");
+        _tagNameMap.put(TagCmWhiteBalance, "CM White Balance");
+        _tagNameMap.put(TagCmWhiteBalanceComp, "CM White Balance Comp");
+        _tagNameMap.put(TagCmWhiteBalanceGrayPoint, "CM White Balance Gray Point");
+        _tagNameMap.put(TagCmSaturation, "CM Saturation");
+        _tagNameMap.put(TagCmHue, "CM Hue");
+        _tagNameMap.put(TagCmContrast, "CM Contrast");
+        _tagNameMap.put(TagCmSharpness, "CM Sharpness");
+    }
+
+    public OlympusRawInfoMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusRawInfoMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Raw Info";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,4 +22,5 @@
 
 import com.drew.lang.ByteArrayReader;
+import com.drew.lang.Charsets;
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.annotations.NotNull;
@@ -29,4 +30,5 @@
 import com.drew.metadata.TagDescriptor;
 
+import java.text.DecimalFormat;
 import java.io.IOException;
 
@@ -45,4 +47,5 @@
  * @author Philipp Sandhaus
  */
+@SuppressWarnings("WeakerAccess")
 public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory>
 {
@@ -109,6 +112,6 @@
             case TAG_TRANSFORM:
                 return getTransformDescription();
-			case TAG_TRANSFORM_1:
-	            return getTransform1Description();
+            case TAG_TRANSFORM_1:
+                return getTransform1Description();
             case TAG_INTELLIGENT_EXPOSURE:
                 return getIntelligentExposureDescription();
@@ -127,6 +130,4 @@
             case TAG_FACE_RECOGNITION_INFO:
                 return getRecognizedFacesDescription();
-            case TAG_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
             case TAG_SCENE_MODE:
                 return getSceneModeDescription();
@@ -134,11 +135,11 @@
                 return getFlashFiredDescription();
             case TAG_TEXT_STAMP:
-		        return getTextStampDescription();
-			case TAG_TEXT_STAMP_1:
-	             return getTextStamp1Description();
-			case TAG_TEXT_STAMP_2:
-		         return getTextStamp2Description();
-			case TAG_TEXT_STAMP_3:
-			     return getTextStamp3Description();
+                return getTextStampDescription();
+            case TAG_TEXT_STAMP_1:
+                return getTextStamp1Description();
+            case TAG_TEXT_STAMP_2:
+                return getTextStamp2Description();
+            case TAG_TEXT_STAMP_3:
+                return getTextStamp3Description();
             case TAG_MAKERNOTE_VERSION:
                 return getMakernoteVersionDescription();
@@ -148,22 +149,57 @@
                 return getInternalSerialNumberDescription();
             case TAG_TITLE:
-	            return getTitleDescription();
-			case TAG_BABY_NAME:
-	            return getBabyNameDescription();
-			case TAG_LOCATION:
-	            return getLocationDescription();
-			case TAG_BABY_AGE:
-		        return getBabyAgeDescription();
-			case TAG_BABY_AGE_1:
-		        return getBabyAge1Description();
-			default:
+                return getTitleDescription();
+            case TAG_BRACKET_SETTINGS:
+                return getBracketSettingsDescription();
+            case TAG_FLASH_CURTAIN:
+                return getFlashCurtainDescription();
+            case TAG_LONG_EXPOSURE_NOISE_REDUCTION:
+                return getLongExposureNoiseReductionDescription();
+            case TAG_BABY_NAME:
+                return getBabyNameDescription();
+            case TAG_LOCATION:
+                return getLocationDescription();
+
+            case TAG_LENS_FIRMWARE_VERSION:
+                return getLensFirmwareVersionDescription();
+            case TAG_INTELLIGENT_D_RANGE:
+                return getIntelligentDRangeDescription();
+            case TAG_CLEAR_RETOUCH:
+                return getClearRetouchDescription();
+            case TAG_PHOTO_STYLE:
+                return getPhotoStyleDescription();
+            case TAG_SHADING_COMPENSATION:
+                return getShadingCompensationDescription();
+
+            case TAG_ACCELEROMETER_Z:
+                return getAccelerometerZDescription();
+            case TAG_ACCELEROMETER_X:
+                return getAccelerometerXDescription();
+            case TAG_ACCELEROMETER_Y:
+                return getAccelerometerYDescription();
+            case TAG_CAMERA_ORIENTATION:
+                return getCameraOrientationDescription();
+            case TAG_ROLL_ANGLE:
+                return getRollAngleDescription();
+            case TAG_PITCH_ANGLE:
+                return getPitchAngleDescription();
+            case TAG_SWEEP_PANORAMA_DIRECTION:
+                return getSweepPanoramaDirectionDescription();
+            case TAG_TIMER_RECORDING:
+                return getTimerRecordingDescription();
+            case TAG_HDR:
+                return getHDRDescription();
+            case TAG_SHUTTER_TYPE:
+                return getShutterTypeDescription();
+            case TAG_TOUCH_AE:
+                return getTouchAeDescription();
+
+            case TAG_BABY_AGE:
+                return getBabyAgeDescription();
+            case TAG_BABY_AGE_1:
+                return getBabyAge1Description();
+            default:
                 return super.getDescription(tagType);
         }
-    }
-
-    @Nullable
-    public String getPrintImageMatchingInfoDescription()
-    {
-        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
     }
 
@@ -279,7 +315,13 @@
 
     @Nullable
+    private static String trim(@Nullable String s)
+    {
+        return s == null ? null : s.trim();
+    }
+
+    @Nullable
     public String getCountryDescription()
     {
-        return getAsciiStringFromBytes(TAG_COUNTRY);
+        return trim(getStringFromBytes(TAG_COUNTRY, Charsets.UTF_8));
     }
 
@@ -287,5 +329,5 @@
     public String getStateDescription()
     {
-        return getAsciiStringFromBytes(TAG_STATE);
+        return trim(getStringFromBytes(TAG_STATE, Charsets.UTF_8));
     }
 
@@ -293,5 +335,5 @@
     public String getCityDescription()
     {
-        return getAsciiStringFromBytes(TAG_CITY);
+        return trim(getStringFromBytes(TAG_CITY, Charsets.UTF_8));
     }
 
@@ -299,17 +341,206 @@
     public String getLandmarkDescription()
     {
-        return getAsciiStringFromBytes(TAG_LANDMARK);
-    }
-
-	@Nullable
+        return trim(getStringFromBytes(TAG_LANDMARK, Charsets.UTF_8));
+    }
+
+    @Nullable
     public String getTitleDescription()
     {
-        return getAsciiStringFromBytes(TAG_TITLE);
-    }
-
-	@Nullable
+        return trim(getStringFromBytes(TAG_TITLE, Charsets.UTF_8));
+    }
+
+    @Nullable
+    public String getBracketSettingsDescription()
+    {
+        return getIndexedDescription(TAG_BRACKET_SETTINGS,
+            "No Bracket", "3 Images, Sequence 0/-/+", "3 Images, Sequence -/0/+", "5 Images, Sequence 0/-/+",
+            "5 Images, Sequence -/0/+", "7 Images, Sequence 0/-/+", "7 Images, Sequence -/0/+");
+    }
+
+    @Nullable
+    public String getFlashCurtainDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_CURTAIN,
+            "n/a", "1st", "2nd");
+    }
+
+    @Nullable
+    public String getLongExposureNoiseReductionDescription()
+    {
+        return getIndexedDescription(TAG_LONG_EXPOSURE_NOISE_REDUCTION, 1,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getLensFirmwareVersionDescription()
+    {
+        // lens version has 4 parts separated by periods
+        byte[] bytes = _directory.getByteArray(TAG_LENS_FIRMWARE_VERSION);
+        if (bytes == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < bytes.length; i++) {
+            sb.append(bytes[i]);
+            if (i < bytes.length - 1)
+                sb.append(".");
+        }
+        return sb.toString();
+        //return string.Join(".", bytes.Select(b => b.ToString()).ToArray());
+    }
+
+    @Nullable
+    public String getIntelligentDRangeDescription()
+    {
+        return getIndexedDescription(TAG_INTELLIGENT_D_RANGE,
+            "Off", "Low", "Standard", "High");
+    }
+
+    @Nullable
+    public String getClearRetouchDescription()
+    {
+        return getIndexedDescription(TAG_CLEAR_RETOUCH,
+                "Off", "On");
+
+    }
+
+    @Nullable
+    public String getPhotoStyleDescription()
+    {
+        return getIndexedDescription(TAG_PHOTO_STYLE,
+            "Auto", "Standard or Custom", "Vivid", "Natural", "Monochrome", "Scenery", "Portrait");
+    }
+
+    @Nullable
+    public String getShadingCompensationDescription()
+    {
+        return getIndexedDescription(TAG_SHADING_COMPENSATION,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getAccelerometerZDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACCELEROMETER_Z);
+        if (value == null)
+            return null;
+
+        // positive is acceleration upwards
+        return String.valueOf(value.shortValue());
+    }
+
+    @Nullable
+    public String getAccelerometerXDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACCELEROMETER_X);
+        if (value == null)
+            return null;
+
+        // positive is acceleration to the left
+        return String.valueOf(value.shortValue());
+    }
+
+    @Nullable
+    public String getAccelerometerYDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACCELEROMETER_Y);
+        if (value == null)
+            return null;
+
+        // positive is acceleration backwards
+        return String.valueOf(value.shortValue());
+    }
+
+    @Nullable
+    public String getCameraOrientationDescription()
+    {
+        return getIndexedDescription(TAG_CAMERA_ORIENTATION,
+                "Normal", "Rotate CW", "Rotate 180", "Rotate CCW", "Tilt Upwards", "Tile Downwards");
+    }
+
+    @Nullable
+    public String getRollAngleDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ROLL_ANGLE);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        // converted to degrees of clockwise camera rotation
+        return format.format(value.shortValue() / 10.0);
+    }
+
+    @Nullable
+    public String getPitchAngleDescription()
+    {
+        Integer value = _directory.getInteger(TAG_PITCH_ANGLE);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        // converted to degrees of upward camera tilt
+        return format.format(-value.shortValue() / 10.0);
+    }
+
+    @Nullable
+    public String getSweepPanoramaDirectionDescription()
+    {
+        return getIndexedDescription(TAG_SWEEP_PANORAMA_DIRECTION,
+                "Off", "Left to Right", "Right to Left", "Top to Bottom", "Bottom to Top");
+    }
+
+    @Nullable
+    public String getTimerRecordingDescription()
+    {
+        return getIndexedDescription(TAG_TIMER_RECORDING,
+                "Off", "Time Lapse", "Stop-motion Animation");
+    }
+
+    @Nullable
+    public String getHDRDescription()
+    {
+        Integer value = _directory.getInteger(TAG_HDR);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "Off";
+            case 100:
+                return "1 EV";
+            case 200:
+                return "2 EV";
+            case 300:
+                return "3 EV";
+            case 32868:
+                return "1 EV (Auto)";
+            case 32968:
+                return "2 EV (Auto)";
+            case 33068:
+                return "3 EV (Auto)";
+            default:
+                return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getShutterTypeDescription()
+    {
+        return getIndexedDescription(TAG_SHUTTER_TYPE,
+                "Mechanical", "Electronic", "Hybrid");
+    }
+
+    @Nullable
+    public String getTouchAeDescription()
+    {
+        return getIndexedDescription(TAG_TOUCH_AE,
+                "Off", "On");
+    }
+
+    @Nullable
     public String getBabyNameDescription()
     {
-        return getAsciiStringFromBytes(TAG_BABY_NAME);
+        return trim(getStringFromBytes(TAG_BABY_NAME, Charsets.UTF_8));
     }
 
@@ -317,5 +548,5 @@
     public String getLocationDescription()
     {
-        return getAsciiStringFromBytes(TAG_LOCATION);
+        return trim(getStringFromBytes(TAG_LOCATION, Charsets.UTF_8));
     }
 
Index: /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -38,4 +38,5 @@
  * @author Philipp Sandhaus
  */
+@SuppressWarnings("WeakerAccess")
 public class PanasonicMakernoteDirectory extends Directory
 {
@@ -354,14 +355,21 @@
     public static final int TAG_FILM_MODE = 0x0042;
 
-    /**
-	 * WB adjust AB. Positive is a shift toward blue.
-	 */
-	public static final int TAG_WB_ADJUST_AB = 0x0046;
-    /**
-	 * WB adjust GM. Positive is a shift toward green.
-	 */
-	public static final int TAG_WB_ADJUST_GM = 0x0047;
-
-
+    public static final int TAG_COLOR_TEMP_KELVIN = 0x0044;
+    public static final int TAG_BRACKET_SETTINGS = 0x0045;
+
+    /**
+    * WB adjust AB. Positive is a shift toward blue.
+    */
+    public static final int TAG_WB_ADJUST_AB = 0x0046;
+    /**
+    * WB adjust GM. Positive is a shift toward green.
+    */
+    public static final int TAG_WB_ADJUST_GM = 0x0047;
+
+    public static final int TAG_FLASH_CURTAIN = 0x0048;
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION = 0x0049;
+
+    public static final int TAG_PANASONIC_IMAGE_WIDTH = 0x004b;
+    public static final int TAG_PANASONIC_IMAGE_HEIGHT = 0x004c;
     public static final int TAG_AF_POINT_POSITION = 0x004d;
 
@@ -384,4 +392,5 @@
     public static final int TAG_LENS_SERIAL_NUMBER = 0x0052;
     public static final int TAG_ACCESSORY_TYPE = 0x0053;
+    public static final int TAG_ACCESSORY_SERIAL_NUMBER = 0x0054;
 
     /**
@@ -403,8 +412,33 @@
     public static final int TAG_INTELLIGENT_EXPOSURE = 0x005d;
 
-    /**
-	  * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-     */
-	public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    public static final int TAG_LENS_FIRMWARE_VERSION = 0x0060;
+    public static final int TAG_BURST_SPEED = 0x0077;
+    public static final int TAG_INTELLIGENT_D_RANGE = 0x0079;
+    public static final int TAG_CLEAR_RETOUCH = 0x007c;
+    public static final int TAG_CITY2 = 0x0080;
+    public static final int TAG_PHOTO_STYLE = 0x0089;
+    public static final int TAG_SHADING_COMPENSATION = 0x008a;
+
+    public static final int TAG_ACCELEROMETER_Z = 0x008c;
+    public static final int TAG_ACCELEROMETER_X = 0x008d;
+    public static final int TAG_ACCELEROMETER_Y = 0x008e;
+    public static final int TAG_CAMERA_ORIENTATION = 0x008f;
+    public static final int TAG_ROLL_ANGLE = 0x0090;
+    public static final int TAG_PITCH_ANGLE = 0x0091;
+    public static final int TAG_SWEEP_PANORAMA_DIRECTION = 0x0093;
+    public static final int TAG_SWEEP_PANORAMA_FIELD_OF_VIEW = 0x0094;
+    public static final int TAG_TIMER_RECORDING = 0x0096;
+
+    public static final int TAG_INTERNAL_ND_FILTER = 0x009d;
+    public static final int TAG_HDR = 0x009e;
+    public static final int TAG_SHUTTER_TYPE = 0x009f;
+
+    public static final int TAG_CLEAR_RETOUCH_VALUE = 0x00a3;
+    public static final int TAG_TOUCH_AE = 0x00ab;
+
+    /**
+    * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+    */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
     /**
@@ -434,7 +468,7 @@
     public static final int TAG_RECOGNIZED_FACE_FLAGS = 0x0063;
     public static final int TAG_TITLE = 0x0065;
-	public static final int TAG_BABY_NAME = 0x0066;
-	public static final int TAG_LOCATION = 0x0067;
-	public static final int TAG_COUNTRY = 0x0069;
+    public static final int TAG_BABY_NAME = 0x0066;
+    public static final int TAG_LOCATION = 0x0067;
+    public static final int TAG_COUNTRY = 0x0069;
     public static final int TAG_STATE = 0x006b;
     public static final int TAG_CITY = 0x006d;
@@ -455,6 +489,6 @@
     public static final int TAG_FLASH_FIRED = 0x8007;
     public static final int TAG_TEXT_STAMP_2 = 0x8008;
-	public static final int TAG_TEXT_STAMP_3 = 0x8009;
-	public static final int TAG_BABY_AGE_1 = 0x8010;
+    public static final int TAG_TEXT_STAMP_3 = 0x8009;
+    public static final int TAG_BABY_AGE_1 = 0x8010;
 
 	/**
@@ -506,5 +540,5 @@
         _tagNameMap.put(TAG_TEXT_STAMP, "Text Stamp");
         _tagNameMap.put(TAG_PROGRAM_ISO, "Program ISO");
-		_tagNameMap.put(TAG_ADVANCED_SCENE_MODE, "Advanced Scene Mode");
+	_tagNameMap.put(TAG_ADVANCED_SCENE_MODE, "Advanced Scene Mode");
         _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
         _tagNameMap.put(TAG_FACES_DETECTED, "Number of Detected Faces");
@@ -512,24 +546,57 @@
         _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
         _tagNameMap.put(TAG_FILM_MODE, "Film Mode");
+        _tagNameMap.put(TAG_COLOR_TEMP_KELVIN, "Color Temp Kelvin");
+        _tagNameMap.put(TAG_BRACKET_SETTINGS, "Bracket Settings");
         _tagNameMap.put(TAG_WB_ADJUST_AB, "White Balance Adjust (AB)");
-		_tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)");
-		_tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position");
+	_tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)");
+
+        _tagNameMap.put(TAG_FLASH_CURTAIN, "Flash Curtain");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_PANASONIC_IMAGE_WIDTH, "Panasonic Image Width");
+        _tagNameMap.put(TAG_PANASONIC_IMAGE_HEIGHT, "Panasonic Image Height");
+
+        _tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position");
         _tagNameMap.put(TAG_FACE_DETECTION_INFO, "Face Detection Info");
         _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
         _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
         _tagNameMap.put(TAG_ACCESSORY_TYPE, "Accessory Type");
+        _tagNameMap.put(TAG_ACCESSORY_SERIAL_NUMBER, "Accessory Serial Number");
         _tagNameMap.put(TAG_TRANSFORM, "Transform");
         _tagNameMap.put(TAG_INTELLIGENT_EXPOSURE, "Intelligent Exposure");
+        _tagNameMap.put(TAG_LENS_FIRMWARE_VERSION, "Lens Firmware Version");
         _tagNameMap.put(TAG_FACE_RECOGNITION_INFO, "Face Recognition Info");
         _tagNameMap.put(TAG_FLASH_WARNING, "Flash Warning");
         _tagNameMap.put(TAG_RECOGNIZED_FACE_FLAGS, "Recognized Face Flags");
-		_tagNameMap.put(TAG_TITLE, "Title");
-		_tagNameMap.put(TAG_BABY_NAME, "Baby Name");
-		_tagNameMap.put(TAG_LOCATION, "Location");
-		_tagNameMap.put(TAG_COUNTRY, "Country");
+        _tagNameMap.put(TAG_TITLE, "Title");
+        _tagNameMap.put(TAG_BABY_NAME, "Baby Name");
+        _tagNameMap.put(TAG_LOCATION, "Location");
+        _tagNameMap.put(TAG_COUNTRY, "Country");
         _tagNameMap.put(TAG_STATE, "State");
         _tagNameMap.put(TAG_CITY, "City");
         _tagNameMap.put(TAG_LANDMARK, "Landmark");
         _tagNameMap.put(TAG_INTELLIGENT_RESOLUTION, "Intelligent Resolution");
+        _tagNameMap.put(TAG_BURST_SPEED, "Burst Speed");
+        _tagNameMap.put(TAG_INTELLIGENT_D_RANGE, "Intelligent D-Range");
+        _tagNameMap.put(TAG_CLEAR_RETOUCH, "Clear Retouch");
+        _tagNameMap.put(TAG_CITY2, "City 2");
+        _tagNameMap.put(TAG_PHOTO_STYLE, "Photo Style");
+        _tagNameMap.put(TAG_SHADING_COMPENSATION, "Shading Compensation");
+
+        _tagNameMap.put(TAG_ACCELEROMETER_Z, "Accelerometer Z");
+        _tagNameMap.put(TAG_ACCELEROMETER_X, "Accelerometer X");
+        _tagNameMap.put(TAG_ACCELEROMETER_Y, "Accelerometer Y");
+        _tagNameMap.put(TAG_CAMERA_ORIENTATION, "Camera Orientation");
+        _tagNameMap.put(TAG_ROLL_ANGLE, "Roll Angle");
+        _tagNameMap.put(TAG_PITCH_ANGLE, "Pitch Angle");
+        _tagNameMap.put(TAG_SWEEP_PANORAMA_DIRECTION, "Sweep Panorama Direction");
+        _tagNameMap.put(TAG_SWEEP_PANORAMA_FIELD_OF_VIEW, "Sweep Panorama Field Of View");
+        _tagNameMap.put(TAG_TIMER_RECORDING, "Timer Recording");
+
+        _tagNameMap.put(TAG_INTERNAL_ND_FILTER, "Internal ND Filter");
+        _tagNameMap.put(TAG_HDR, "HDR");
+        _tagNameMap.put(TAG_SHUTTER_TYPE, "Shutter Type");
+        _tagNameMap.put(TAG_CLEAR_RETOUCH_VALUE, "Clear Retouch Value");
+        _tagNameMap.put(TAG_TOUCH_AE, "Touch AE");
+
         _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
         _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
@@ -538,9 +605,9 @@
         _tagNameMap.put(TAG_WB_BLUE_LEVEL, "White Balance (Blue)");
         _tagNameMap.put(TAG_FLASH_FIRED, "Flash Fired");
-		_tagNameMap.put(TAG_TEXT_STAMP_1, "Text Stamp 1");
-		_tagNameMap.put(TAG_TEXT_STAMP_2, "Text Stamp 2");
-		_tagNameMap.put(TAG_TEXT_STAMP_3, "Text Stamp 3");
-		_tagNameMap.put(TAG_BABY_AGE_1, "Baby Age 1");
-		_tagNameMap.put(TAG_TRANSFORM_1, "Transform 1");
+        _tagNameMap.put(TAG_TEXT_STAMP_1, "Text Stamp 1");
+        _tagNameMap.put(TAG_TEXT_STAMP_2, "Text Stamp 2");
+        _tagNameMap.put(TAG_TEXT_STAMP_3, "Text Stamp 3");
+        _tagNameMap.put(TAG_BABY_AGE_1, "Baby Age 1");
+        _tagNameMap.put(TAG_TRANSFORM_1, "Transform 1");
     }
 
Index: /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,4 +35,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PentaxMakernoteDescriptor extends TagDescriptor<PentaxMakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PentaxMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2017 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.StringValue;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import static com.drew.metadata.exif.makernotes.ReconyxHyperFireMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ReconyxHyperFireMakernoteDirectory}.
+ *
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxHyperFireMakernoteDescriptor extends TagDescriptor<ReconyxHyperFireMakernoteDirectory>
+{
+    public ReconyxHyperFireMakernoteDescriptor(@NotNull ReconyxHyperFireMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_VERSION:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_FIRMWARE_VERSION:
+                return _directory.getString(tagType);
+            case TAG_TRIGGER_MODE:
+                return _directory.getString(tagType);
+            case TAG_SEQUENCE:
+                int[] sequence = _directory.getIntArray(tagType);
+                if (sequence == null)
+                    return null;
+                return String.format("%d/%d", sequence[0], sequence[1]);
+            case TAG_EVENT_NUMBER:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_MOTION_SENSITIVITY:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_BATTERY_VOLTAGE:
+                Double value = _directory.getDoubleObject(tagType);
+                DecimalFormat formatter = new DecimalFormat("0.000");
+                return value == null ? null : formatter.format(value);
+            case TAG_DATE_TIME_ORIGINAL:
+                String date = _directory.getString(tagType);
+                try {
+                    DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+                    return parser.format(parser.parse(date));
+                } catch (ParseException e) {
+                    return null;
+                }
+            case TAG_MOON_PHASE:
+                return getIndexedDescription(tagType, "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent");
+            case TAG_AMBIENT_TEMPERATURE_FAHRENHEIT:
+            case TAG_AMBIENT_TEMPERATURE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_SERIAL_NUMBER:
+                // default is UTF_16LE
+                StringValue svalue = _directory.getStringValue(tagType);
+                if(svalue == null)
+                    return null;
+                return svalue.toString();
+            case TAG_CONTRAST:
+            case TAG_BRIGHTNESS:
+            case TAG_SHARPNESS:
+            case TAG_SATURATION:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_INFRARED_ILLUMINATOR:
+                return getIndexedDescription(tagType, "Off", "On");
+            case TAG_USER_LABEL:
+                return _directory.getString(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Reconyx HyperFire cameras.
+ *
+ * Reconyx uses a fixed makernote block.  Tag values are the byte index of the tag within the makernote.
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxHyperFireMakernoteDirectory extends Directory
+{
+    /**
+     * Version number used for identifying makernotes from Reconyx HyperFire cameras.
+     */
+    public static final int MAKERNOTE_VERSION = 61697;
+
+    public static final int TAG_MAKERNOTE_VERSION = 0;
+    public static final int TAG_FIRMWARE_VERSION = 2;
+    public static final int TAG_TRIGGER_MODE = 12;
+    public static final int TAG_SEQUENCE = 14;
+    public static final int TAG_EVENT_NUMBER = 18;
+    public static final int TAG_DATE_TIME_ORIGINAL = 22;
+    public static final int TAG_MOON_PHASE = 36;
+    public static final int TAG_AMBIENT_TEMPERATURE_FAHRENHEIT = 38;
+    public static final int TAG_AMBIENT_TEMPERATURE = 40;
+    public static final int TAG_SERIAL_NUMBER = 42;
+    public static final int TAG_CONTRAST = 72;
+    public static final int TAG_BRIGHTNESS = 74;
+    public static final int TAG_SHARPNESS = 76;
+    public static final int TAG_SATURATION = 78;
+    public static final int TAG_INFRARED_ILLUMINATOR = 80;
+    public static final int TAG_MOTION_SENSITIVITY = 82;
+    public static final int TAG_BATTERY_VOLTAGE = 84;
+    public static final int TAG_USER_LABEL = 86;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_TRIGGER_MODE, "Trigger Mode");
+        _tagNameMap.put(TAG_SEQUENCE, "Sequence");
+        _tagNameMap.put(TAG_EVENT_NUMBER, "Event Number");
+        _tagNameMap.put(TAG_DATE_TIME_ORIGINAL, "Date/Time Original");
+        _tagNameMap.put(TAG_MOON_PHASE, "Moon Phase");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, "Ambient Temperature Fahrenheit");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE, "Ambient Temperature");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_BRIGHTNESS, "Brightness");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_INFRARED_ILLUMINATOR, "Infrared Illuminator");
+        _tagNameMap.put(TAG_MOTION_SENSITIVITY, "Motion Sensitivity");
+        _tagNameMap.put(TAG_BATTERY_VOLTAGE, "Battery Voltage");
+        _tagNameMap.put(TAG_USER_LABEL, "User Label");
+    }
+
+    public ReconyxHyperFireMakernoteDirectory()
+    {
+        this.setDescriptor(new ReconyxHyperFireMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Reconyx HyperFire Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-2017 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.StringValue;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import static com.drew.metadata.exif.makernotes.ReconyxUltraFireMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ReconyxUltraFireMakernoteDirectory}.
+ *
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxUltraFireMakernoteDescriptor extends TagDescriptor<ReconyxUltraFireMakernoteDirectory>
+{
+    public ReconyxUltraFireMakernoteDescriptor(@NotNull ReconyxUltraFireMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_LABEL:
+                return _directory.getString(tagType);
+            case TAG_MAKERNOTE_ID:
+                return String.format("0x%08X", _directory.getInteger(tagType));
+            case TAG_MAKERNOTE_SIZE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_MAKERNOTE_PUBLIC_ID:
+                return String.format("0x%08X", _directory.getInteger(tagType));
+            case TAG_MAKERNOTE_PUBLIC_SIZE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_CAMERA_VERSION:
+            case TAG_UIB_VERSION:
+            case TAG_BTL_VERSION:
+            case TAG_PEX_VERSION:
+            case TAG_EVENT_TYPE:
+                return _directory.getString(tagType);
+            case TAG_SEQUENCE:
+                int[] sequence = _directory.getIntArray(tagType);
+                if (sequence == null)
+                    return null;
+                return String.format("%d/%d", sequence[0], sequence[1]);
+            case TAG_EVENT_NUMBER:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_DATE_TIME_ORIGINAL:
+                String date = _directory.getString(tagType);
+                try {
+                    DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+                    return parser.format(parser.parse(date));
+                } catch (ParseException e) {
+                    return null;
+                }
+            /*case TAG_DAY_OF_WEEK:
+                return getIndexedDescription(tagType, CultureInfo.CurrentCulture.DateTimeFormat.DayNames);*/
+            case TAG_MOON_PHASE:
+                return getIndexedDescription(tagType, "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent");
+            case TAG_AMBIENT_TEMPERATURE_FAHRENHEIT:
+            case TAG_AMBIENT_TEMPERATURE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_FLASH:
+                return getIndexedDescription(tagType, "Off", "On");
+            case TAG_BATTERY_VOLTAGE:
+                Double value = _directory.getDoubleObject(tagType);
+                DecimalFormat formatter = new DecimalFormat("0.000");
+                return value == null ? null : formatter.format(value);
+            case TAG_SERIAL_NUMBER:
+                // default is UTF_8
+                StringValue svalue = _directory.getStringValue(tagType);
+                if(svalue == null)
+                    return null;
+                return svalue.toString();
+            case TAG_USER_LABEL:
+                return _directory.getString(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Reconyx UltraFire cameras.
+ *
+ * Reconyx uses a fixed makernote block.  Tag values are the byte index of the tag within the makernote.
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxUltraFireMakernoteDirectory extends Directory
+{
+    /**
+     * Version number used for identifying makernotes from Reconyx UltraFire cameras.
+     */
+    public static final int MAKERNOTE_ID = 0x00010000;
+
+    /**
+     * Version number used for identifying the public portion of makernotes from Reconyx UltraFire cameras.
+     */
+    public static final int MAKERNOTE_PUBLIC_ID = 0x07f10001;
+
+    public static final int TAG_LABEL = 0;
+    public static final int TAG_MAKERNOTE_ID = 10;
+    public static final int TAG_MAKERNOTE_SIZE = 14;
+    public static final int TAG_MAKERNOTE_PUBLIC_ID = 18;
+    public static final int TAG_MAKERNOTE_PUBLIC_SIZE = 22;
+    public static final int TAG_CAMERA_VERSION = 24;
+    public static final int TAG_UIB_VERSION = 31;
+    public static final int TAG_BTL_VERSION = 38;
+    public static final int TAG_PEX_VERSION = 45;
+    public static final int TAG_EVENT_TYPE = 52;
+    public static final int TAG_SEQUENCE = 53;
+    public static final int TAG_EVENT_NUMBER = 55;
+    public static final int TAG_DATE_TIME_ORIGINAL = 59;
+    public static final int TAG_DAY_OF_WEEK = 66;
+    public static final int TAG_MOON_PHASE = 67;
+    public static final int TAG_AMBIENT_TEMPERATURE_FAHRENHEIT = 68;
+    public static final int TAG_AMBIENT_TEMPERATURE = 70;
+    public static final int TAG_FLASH = 72;
+    public static final int TAG_BATTERY_VOLTAGE = 73;
+    public static final int TAG_SERIAL_NUMBER = 75;
+    public static final int TAG_USER_LABEL = 80;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_LABEL, "Makernote Label");
+        _tagNameMap.put(TAG_MAKERNOTE_ID, "Makernote ID");
+        _tagNameMap.put(TAG_MAKERNOTE_SIZE, "Makernote Size");
+        _tagNameMap.put(TAG_MAKERNOTE_PUBLIC_ID, "Makernote Public ID");
+        _tagNameMap.put(TAG_MAKERNOTE_PUBLIC_SIZE, "Makernote Public Size");
+        _tagNameMap.put(TAG_CAMERA_VERSION, "Camera Version");
+        _tagNameMap.put(TAG_UIB_VERSION, "Uib Version");
+        _tagNameMap.put(TAG_BTL_VERSION, "Btl Version");
+        _tagNameMap.put(TAG_PEX_VERSION, "Pex Version");
+        _tagNameMap.put(TAG_EVENT_TYPE, "Event Type");
+        _tagNameMap.put(TAG_SEQUENCE, "Sequence");
+        _tagNameMap.put(TAG_EVENT_NUMBER, "Event Number");
+        _tagNameMap.put(TAG_DATE_TIME_ORIGINAL, "Date/Time Original");
+        _tagNameMap.put(TAG_DAY_OF_WEEK, "Day of Week");
+        _tagNameMap.put(TAG_MOON_PHASE, "Moon Phase");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, "Ambient Temperature Fahrenheit");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE, "Ambient Temperature");
+        _tagNameMap.put(TAG_FLASH, "Flash");
+        _tagNameMap.put(TAG_BATTERY_VOLTAGE, "Battery Voltage");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_USER_LABEL, "User Label");
+    }
+
+    public ReconyxUltraFireMakernoteDirectory()
+    {
+        this.setDescriptor(new ReconyxUltraFireMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Reconyx UltraFire Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,5 +26,5 @@
 
 /**
- * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDescriptor}.
+ * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDirectory}.
  * <p>
  * Some information about this makernote taken from here:
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirectory>
 {
@@ -45,6 +46,4 @@
     {
         switch (tagType) {
-//            case TAG_PRINT_IMAGE_MATCHING_INFO:
-//                return getPrintImageMatchingInfoDescription();
 //            case TAG_PROPRIETARY_THUMBNAIL:
 //                return getProprietaryThumbnailDataDescription();
@@ -53,10 +52,4 @@
         }
     }
-
-//    @Nullable
-//    public String getPrintImageMatchingInfoDescription()
-//    {
-//        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
-//    }
 //
 //    @Nullable
Index: /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class RicohMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java	(revision 13061)
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SamsungType2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SamsungType2MakernoteDirectory}.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Samsung.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class SamsungType2MakernoteDescriptor extends TagDescriptor<SamsungType2MakernoteDirectory>
+{
+    public SamsungType2MakernoteDescriptor(@NotNull SamsungType2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagMakerNoteVersion:
+                return getMakernoteVersionDescription();
+            case TagDeviceType:
+                return getDeviceTypeDescription();
+            case TagSamsungModelId:
+                return getSamsungModelIdDescription();
+
+            case TagCameraTemperature:
+                return getCameraTemperatureDescription();
+
+            case TagFaceDetect:
+                return getFaceDetectDescription();
+            case TagFaceRecognition:
+                return getFaceRecognitionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TagMakerNoteVersion, 2);
+    }
+
+    @Nullable
+    public String getDeviceTypeDescription()
+    {
+        Integer value = _directory.getInteger(TagDeviceType);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0x1000:
+                return "Compact Digital Camera";
+            case 0x2000:
+                return "High-end NX Camera";
+            case 0x3000:
+                return "HXM Video Camera";
+            case 0x12000:
+                return "Cell Phone";
+            case 0x300000:
+                return "SMX Video Camera";
+            default:
+                return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getSamsungModelIdDescription()
+    {
+        Integer value = _directory.getInteger(TagSamsungModelId);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0x100101c:
+                return "NX10";
+            /*case 0x1001226:
+                    return "HMX-S10BP";*/
+            case 0x1001226:
+                    return "HMX-S15BP";
+            case 0x1001233:
+                    return "HMX-Q10";
+            /*case 0x1001234:
+                    return "HMX-H300";*/
+            case 0x1001234:
+                    return "HMX-H304";
+            case 0x100130c:
+                    return "NX100";
+            case 0x1001327:
+                    return "NX11";
+            case 0x170104e:
+                    return "ES70, ES71 / VLUU ES70, ES71 / SL600";
+            case 0x1701052:
+                    return "ES73 / VLUU ES73 / SL605";
+            case 0x1701300:
+                    return "ES28 / VLUU ES28";
+            case 0x1701303:
+                    return "ES74,ES75,ES78 / VLUU ES75,ES78";
+            case 0x2001046:
+                    return "PL150 / VLUU PL150 / TL210 / PL151";
+            case 0x2001311:
+                    return "PL120,PL121 / VLUU PL120,PL121";
+            case 0x2001315:
+                    return "PL170,PL171 / VLUUPL170,PL171";
+            case 0x200131e:
+                    return "PL210, PL211 / VLUU PL210, PL211";
+            case 0x2701317:
+                    return "PL20,PL21 / VLUU PL20,PL21";
+            case 0x2a0001b:
+                    return "WP10 / VLUU WP10 / AQ100";
+            case 0x3000000:
+                    return "Various Models (0x3000000)";
+            case 0x3a00018:
+                    return "Various Models (0x3a00018)";
+            case 0x400101f:
+                    return "ST1000 / ST1100 / VLUU ST1000 / CL65";
+            case 0x4001022:
+                    return "ST550 / VLUU ST550 / TL225";
+            case 0x4001025:
+                    return "Various Models (0x4001025)";
+            case 0x400103e:
+                    return "VLUU ST5500, ST5500, CL80";
+            case 0x4001041:
+                    return "VLUU ST5000, ST5000, TL240";
+            case 0x4001043:
+                    return "ST70 / VLUU ST70 / ST71";
+            case 0x400130a:
+                    return "Various Models (0x400130a)";
+            case 0x400130e:
+                    return "ST90,ST91 / VLUU ST90,ST91";
+            case 0x4001313:
+                    return "VLUU ST95, ST95";
+            case 0x4a00015:
+                    return "VLUU ST60";
+            case 0x4a0135b:
+                    return "ST30, ST65 / VLUU ST65 / ST67";
+            case 0x5000000:
+                    return "Various Models (0x5000000)";
+            case 0x5001038:
+                    return "Various Models (0x5001038)";
+            case 0x500103a:
+                    return "WB650 / VLUU WB650 / WB660";
+            case 0x500103c:
+                    return "WB600 / VLUU WB600 / WB610";
+            case 0x500133e:
+                    return "WB150 / WB150F / WB152 / WB152F / WB151";
+            case 0x5a0000f:
+                    return "WB5000 / HZ25W";
+            case 0x6001036:
+                    return "EX1";
+            case 0x700131c:
+                    return "VLUU SH100, SH100";
+            case 0x27127002:
+                    return "SMX - C20N";
+            default:
+                return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    private String getCameraTemperatureDescription()
+    {
+        return getFormattedInt(TagCameraTemperature, "%d C");
+    }
+
+    @Nullable
+    public String getFaceDetectDescription()
+    {
+        return getIndexedDescription(TagFaceDetect,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getFaceRecognitionDescription()
+    {
+        return getIndexedDescription(TagFaceRecognition,
+            "Off", "On");
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java	(revision 13061)
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific certain 'newer' Samsung cameras.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Samsung.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class SamsungType2MakernoteDirectory extends Directory
+{
+    // This list is incomplete
+    public static final int TagMakerNoteVersion = 0x001;
+    public static final int TagDeviceType = 0x0002;
+    public static final int TagSamsungModelId = 0x0003;
+
+    public static final int TagCameraTemperature = 0x0043;
+
+    public static final int TagFaceDetect = 0x0100;
+    public static final int TagFaceRecognition = 0x0120;
+    public static final int TagFaceName = 0x0123;
+
+    // following tags found only in SRW images
+    public static final int TagFirmwareName = 0xa001;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagMakerNoteVersion, "Maker Note Version");
+        _tagNameMap.put(TagDeviceType, "Device Type");
+        _tagNameMap.put(TagSamsungModelId, "Model Id");
+
+        _tagNameMap.put(TagCameraTemperature, "Camera Temperature");
+
+        _tagNameMap.put(TagFaceDetect, "Face Detect");
+        _tagNameMap.put(TagFaceRecognition, "Face Recognition");
+        _tagNameMap.put(TagFaceName, "Face Name");
+        _tagNameMap.put(TagFirmwareName, "Firmware Name");
+    }
+
+    public SamsungType2MakernoteDirectory()
+    {
+        this.setDescriptor(new SamsungType2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Samsung Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SanyoMakernoteDescriptor extends TagDescriptor<SanyoMakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,4 +32,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SanyoMakernoteDirectory extends Directory
 {
@@ -62,5 +63,5 @@
     public static final int TAG_FLASH_MODE = 0x0225;
 
-    public static final int TAG_PRINT_IM = 0x0e00;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
     public static final int TAG_DATA_DUMP = 0x0f00;
@@ -99,5 +100,5 @@
         _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
 
-        _tagNameMap.put(TAG_PRINT_IM, "Print IM");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print IM");
 
         _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
Index: /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SigmaMakernoteDescriptor extends TagDescriptor<SigmaMakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,4 +32,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SigmaMakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1MakernoteDirectory>
 {
@@ -433,5 +434,13 @@
     public String getVignettingCorrectionDescription()
     {
-        return getIndexedDescription(TAG_VIGNETTING_CORRECTION, "Off", null, "Auto");
+        Integer value = _directory.getInteger(TAG_VIGNETTING_CORRECTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 2: return "Auto";
+            case 0xffffffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
     }
 
@@ -439,5 +448,13 @@
     public String getLateralChromaticAberrationDescription()
     {
-        return getIndexedDescription(TAG_LATERAL_CHROMATIC_ABERRATION, "Off", null, "Auto");
+        Integer value = _directory.getInteger(TAG_LATERAL_CHROMATIC_ABERRATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 2: return "Auto";
+            case 0xffffffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
     }
 
@@ -445,5 +462,13 @@
     public String getDistortionCorrectionDescription()
     {
-        return getIndexedDescription(TAG_DISTORTION_CORRECTION, "Off", null, "Auto");
+        Integer value = _directory.getInteger(TAG_DISTORTION_CORRECTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 2: return "Auto";
+            case 0xffffffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
     }
 
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType1MakernoteDirectory extends Directory
 {
@@ -141,5 +142,5 @@
         _tagNameMap.put(TAG_EXTRA_INFO, "Extra Info");
 
-        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching Info");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
 
         _tagNameMap.put(TAG_MULTI_BURST_MODE, "Multi Burst Mode");
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType6MakernoteDescriptor extends TagDescriptor<SonyType6MakernoteDirectory>
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,4 +32,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType6MakernoteDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/exif/makernotes/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/makernotes/package-info.java	(revision 13061)
@@ -0,0 +1,5 @@
+/**
+ * Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the
+ * modelling of manufacturer-specific makernotes.
+ */
+package com.drew.metadata.exif.makernotes;
Index: unk/src/com/drew/metadata/exif/makernotes/package.html
===================================================================
--- /trunk/src/com/drew/metadata/exif/makernotes/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the modelling of manufacturer-specific makernotes.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/exif/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/exif/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/exif/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes.
+ */
+package com.drew.metadata.exif;
Index: unk/src/com/drew/metadata/exif/package.html
===================================================================
--- /trunk/src/com/drew/metadata/exif/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,4 +30,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class FileMetadataDescriptor extends TagDescriptor<FileMetadataDirectory>
 {
Index: /trunk/src/com/drew/metadata/file/FileMetadataDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,4 +29,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class FileMetadataDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/file/FileMetadataReader.java
===================================================================
--- /trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: /trunk/src/com/drew/metadata/file/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/file/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/file/package-info.java	(revision 13061)
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of file system metadata.
+ *
+ * @since 2.8.0
+ */
+package com.drew.metadata.file;
Index: unk/src/com/drew/metadata/file/package.html
===================================================================
--- /trunk/src/com/drew/metadata/file/package.html	(revision 13060)
+++ 	(revision )
@@ -1,34 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of file system metadata.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.8.0
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/iptc/IptcDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,4 +35,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class IptcDescriptor extends TagDescriptor<IptcDirectory>
 {
Index: /trunk/src/com/drew/metadata/iptc/IptcDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -38,4 +38,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class IptcDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/iptc/IptcReader.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,6 +29,8 @@
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
 import java.io.IOException;
+import java.nio.charset.Charset;
 import java.util.Collections;
 
@@ -56,4 +58,5 @@
     public static final int POST_DATA_RECORD = 9;
 */
+    private static final byte IptcMarkerByte = 0x1c;
 
     @NotNull
@@ -67,5 +70,5 @@
         for (byte[] segmentBytes : segments) {
             // Ensure data starts with the IPTC marker byte
-            if (segmentBytes.length != 0 && segmentBytes[0] == 0x1c) {
+            if (segmentBytes.length != 0 && segmentBytes[0] == IptcMarkerByte) {
                 extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
             }
@@ -107,9 +110,9 @@
             }
 
-            if (startByte != 0x1c) {
+            if (startByte != IptcMarkerByte) {
                 // NOTE have seen images where there was one extra byte at the end, giving
                 // offset==length at this point, which is not worth logging as an error.
                 if (offset != length)
-                    directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x1c' but got '0x" + Integer.toHexString(startByte) + "'.");
+                    directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x" + Integer.toHexString(IptcMarkerByte) + "' but got '0x" + Integer.toHexString(startByte) + "'.");
                 return;
             }
@@ -164,16 +167,13 @@
         }
 
-        String string = null;
-
         switch (tagIdentifier) {
             case IptcDirectory.TAG_CODED_CHARACTER_SET:
                 byte[] bytes = reader.getBytes(tagByteCount);
-                String charset = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes);
-                if (charset == null) {
+                String charsetName = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes);
+                if (charsetName == null) {
                     // Unable to determine the charset, so fall through and treat tag as a regular string
-                    string = new String(bytes);
-                    break;
+                    charsetName = new String(bytes);
                 }
-                directory.setString(tagIdentifier, charset);
+                directory.setString(tagIdentifier, charsetName);
                 return;
             case IptcDirectory.TAG_ENVELOPE_RECORD_VERSION:
@@ -201,30 +201,36 @@
         // If we haven't returned yet, treat it as a string
         // NOTE that there's a chance we've already loaded the value as a string above, but failed to parse the value
-        if (string == null) {
-            String encoding = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET);
-            if (encoding != null) {
-                string = reader.getString(tagByteCount, encoding);
-            } else {
-                byte[] bytes = reader.getBytes(tagByteCount);
-                encoding = Iso2022Converter.guessEncoding(bytes);
-                string = encoding != null ? new String(bytes, encoding) : new String(bytes);
-            }
+        String charSetName = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET);
+        Charset charset = null;
+        try {
+            if (charSetName != null)
+                charset = Charset.forName(charSetName);
+        } catch (Throwable ignored) {
+        }
+
+        StringValue string;
+        if (charSetName != null) {
+            string = reader.getStringValue(tagByteCount, charset);
+        } else {
+            byte[] bytes = reader.getBytes(tagByteCount);
+            Charset charSet = Iso2022Converter.guessCharSet(bytes);
+            string = charSet != null ? new StringValue(bytes, charSet) : new StringValue(bytes, null);
         }
 
         if (directory.containsTag(tagIdentifier)) {
-            // this fancy string[] business avoids using an ArrayList for performance reasons
-            String[] oldStrings = directory.getStringArray(tagIdentifier);
-            String[] newStrings;
+            // this fancy StringValue[] business avoids using an ArrayList for performance reasons
+            StringValue[] oldStrings = directory.getStringValueArray(tagIdentifier);
+            StringValue[] newStrings;
             if (oldStrings == null) {
                 // TODO hitting this block means any prior value(s) are discarded
-                newStrings = new String[1];
+                newStrings = new StringValue[1];
             } else {
-                newStrings = new String[oldStrings.length + 1];
+                newStrings = new StringValue[oldStrings.length + 1];
                 System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length);
             }
             newStrings[newStrings.length - 1] = string;
-            directory.setStringArray(tagIdentifier, newStrings);
+            directory.setStringValueArray(tagIdentifier, newStrings);
         } else {
-            directory.setString(tagIdentifier, string);
+            directory.setStringValue(tagIdentifier, string);
         }
     }
Index: /trunk/src/com/drew/metadata/iptc/Iso2022Converter.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -59,7 +59,7 @@
 
     /**
-     * Attempts to guess the encoding of a string provided as a byte array.
+     * Attempts to guess the {@link Charset} of a string provided as a byte array.
      * <p>
-     * Encodings trialled are, in order:
+     * Charsets trialled are, in order:
      * <ul>
      *     <li>UTF-8</li>
@@ -68,5 +68,5 @@
      * </ul>
      * <p>
-     * Its only purpose is to guess the encoding if and only if iptc tag coded character set is not set. If the
+     * Its only purpose is to guess the Charset 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.
@@ -79,5 +79,5 @@
      */
     @Nullable
-    static String guessEncoding(@NotNull final byte[] bytes)
+    static Charset guessCharSet(@NotNull final byte[] bytes)
     {
         String[] encodings = { UTF_8, System.getProperty("file.encoding"), ISO_8859_1 };
@@ -85,9 +85,10 @@
         for (String encoding : encodings)
         {
-            CharsetDecoder cs = Charset.forName(encoding).newDecoder();
+            Charset charset = Charset.forName(encoding);
+            CharsetDecoder cs = charset.newDecoder();
 
             try {
                 cs.decode(ByteBuffer.wrap(bytes));
-                return encoding;
+                return charset;
             } catch (CharacterCodingException e) {
                 // fall through...
Index: /trunk/src/com/drew/metadata/iptc/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/iptc/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/iptc/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of IPTC metadata.
+ */
+package com.drew.metadata.iptc;
Index: unk/src/com/drew/metadata/iptc/package.html
===================================================================
--- /trunk/src/com/drew/metadata/iptc/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of IPTC metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java	(revision 13061)
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2017 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.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.jpeg.HuffmanTablesDirectory.*;
+
+/**
+ * Provides a human-readable string version of the tag stored in a HuffmanTableDirectory.
+ *
+ * <ul>
+ *   <li>https://en.wikipedia.org/wiki/Huffman_coding</li>
+ *   <li>http://stackoverflow.com/a/4954117</li>
+ * </ul>
+ *
+ * @author Nadahar
+ */
+@SuppressWarnings("WeakerAccess")
+public class HuffmanTablesDescriptor extends TagDescriptor<HuffmanTablesDirectory>
+{
+    public HuffmanTablesDescriptor(@NotNull HuffmanTablesDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_NUMBER_OF_TABLES:
+                return getNumberOfTablesDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getNumberOfTablesDescription()
+    {
+        Integer value = _directory.getInteger(TAG_NUMBER_OF_TABLES);
+        if (value==null)
+            return null;
+        return value + (value == 1 ? " Huffman table" : " Huffman tables");
+    }
+}
Index: /trunk/src/com/drew/metadata/jpeg/HuffmanTablesDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/HuffmanTablesDirectory.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/jpeg/HuffmanTablesDirectory.java	(revision 13061)
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2002-2017 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.jpeg;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.MetadataException;
+
+/**
+ * Directory of tables for the DHT (Define Huffman Table(s)) segment.
+ *
+ * @author Nadahar
+ */
+@SuppressWarnings("WeakerAccess")
+public class HuffmanTablesDirectory extends Directory {
+
+    public static final int TAG_NUMBER_OF_TABLES = 1;
+
+    protected static final byte[] TYPICAL_LUMINANCE_DC_LENGTHS = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01,
+        (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
+    };
+
+    protected static final byte[] TYPICAL_LUMINANCE_DC_VALUES = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
+        (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_DC_LENGTHS = {
+        (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01,
+        (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_DC_VALUES = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
+        (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B
+    };
+
+    protected static final byte[] TYPICAL_LUMINANCE_AC_LENGTHS = {
+        (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x03, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x03,
+        (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x7D
+    };
+
+    protected static final byte[] TYPICAL_LUMINANCE_AC_VALUES = {
+        (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x11, (byte) 0x05, (byte) 0x12,
+        (byte) 0x21, (byte) 0x31, (byte) 0x41, (byte) 0x06, (byte) 0x13, (byte) 0x51, (byte) 0x61, (byte) 0x07,
+        (byte) 0x22, (byte) 0x71, (byte) 0x14, (byte) 0x32, (byte) 0x81, (byte) 0x91, (byte) 0xA1, (byte) 0x08,
+        (byte) 0x23, (byte) 0x42, (byte) 0xB1, (byte) 0xC1, (byte) 0x15, (byte) 0x52, (byte) 0xD1, (byte) 0xF0,
+        (byte) 0x24, (byte) 0x33, (byte) 0x62, (byte) 0x72, (byte) 0x82, (byte) 0x09, (byte) 0x0A, (byte) 0x16,
+        (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28,
+        (byte) 0x29, (byte) 0x2A, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39,
+        (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49,
+        (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59,
+        (byte) 0x5A, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69,
+        (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79,
+        (byte) 0x7A, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89,
+        (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98,
+        (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7,
+        (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6,
+        (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5,
+        (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4,
+        (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE1, (byte) 0xE2,
+        (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA,
+        (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8,
+        (byte) 0xF9, (byte) 0xFA
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_AC_LENGTHS = {
+        (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x03, (byte) 0x04,
+        (byte) 0x07, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x77
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_AC_VALUES = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x11, (byte) 0x04, (byte) 0x05, (byte) 0x21,
+        (byte) 0x31, (byte) 0x06, (byte) 0x12, (byte) 0x41, (byte) 0x51, (byte) 0x07, (byte) 0x61, (byte) 0x71,
+        (byte) 0x13, (byte) 0x22, (byte) 0x32, (byte) 0x81, (byte) 0x08, (byte) 0x14, (byte) 0x42, (byte) 0x91,
+        (byte) 0xA1, (byte) 0xB1, (byte) 0xC1, (byte) 0x09, (byte) 0x23, (byte) 0x33, (byte) 0x52, (byte) 0xF0,
+        (byte) 0x15, (byte) 0x62, (byte) 0x72, (byte) 0xD1, (byte) 0x0A, (byte) 0x16, (byte) 0x24, (byte) 0x34,
+        (byte) 0xE1, (byte) 0x25, (byte) 0xF1, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x26,
+        (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38,
+        (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48,
+        (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58,
+        (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68,
+        (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78,
+        (byte) 0x79, (byte) 0x7A, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87,
+        (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96,
+        (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5,
+        (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4,
+        (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3,
+        (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2,
+        (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA,
+        (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9,
+        (byte) 0xEA, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8,
+        (byte) 0xF9, (byte) 0xFA
+    };
+
+    @NotNull
+    protected final List<HuffmanTable> tables = new ArrayList<HuffmanTable>(4);
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_NUMBER_OF_TABLES, "Number of Tables");
+    }
+
+    public HuffmanTablesDirectory()
+    {
+        this.setDescriptor(new HuffmanTablesDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Huffman";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * @param tableNumber The zero-based index of the table. This number is normally between 0 and 3.
+     *                    Use {@link #getNumberOfTables} for bounds-checking.
+     * @return The {@link HuffmanTable} having the specified number.
+     */
+    @NotNull
+    public HuffmanTable getTable(int tableNumber)
+    {
+        return tables.get(tableNumber);
+    }
+
+    /**
+     * @return The number of Huffman tables held by this {@link HuffmanTablesDirectory} instance.
+     */
+    public int getNumberOfTables() throws MetadataException
+    {
+        return getInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES);
+    }
+
+    /**
+     * @return The {@link List} of {@link HuffmanTable}s in this
+     *         {@link Directory}.
+     */
+    @NotNull
+    protected List<HuffmanTable> getTables() {
+        return tables;
+    }
+
+    /**
+     * Evaluates whether all the tables in this {@link HuffmanTablesDirectory}
+     * are "typical" Huffman tables.
+     * <p>
+     * "Typical" has a special meaning in this context as the JPEG standard
+     * (ISO/IEC 10918 or ITU-T T.81) defines 4 Huffman tables that has been
+     * developed from the average statistics of a large set of images with 8-bit
+     * precision. Using these instead of calculating the optimal Huffman tables
+     * for a given image is faster, and is preferred by many hardware encoders
+     * and some hardware decoders.
+     * <p>
+     * Even though the JPEG standard doesn't define these as "standard tables"
+     * and requires a decoder to be able to read any valid Huffman tables, some
+     * are in reality limited decoding images using these "typical" tables.
+     * Standards like DCF (Design rule for Camera File system) and DLNA (Digital
+     * Living Network Alliance) actually requires any compliant JPEG to use only
+     * the "typical" Huffman tables.
+     * <p>
+     * This is also related to the term "optimized" JPEG. An "optimized" JPEG is
+     * a JPEG that doesn't use the "typical" Huffman tables.
+     *
+     * @return Whether or not all the tables in this
+     *         {@link HuffmanTablesDirectory} are the predefined "typical"
+     *         Huffman tables.
+     */
+    public boolean isTypical() {
+        if (tables.size() == 0) {
+            return false;
+        }
+        for (HuffmanTable table : tables) {
+            if (!table.isTypical()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * The opposite of {@link #isTypical()}.
+     *
+     * @return Whether or not the tables in this {@link HuffmanTablesDirectory}
+     *         are "optimized" - which means that at least one of them aren't
+     *         one of the "typical" Huffman tables.
+     */
+    public boolean isOptimized() {
+        return !isTypical();
+    }
+
+    /**
+     * An instance of this class holds a JPEG Huffman table.
+     */
+    public static class HuffmanTable {
+        private final int tableLength;
+        private final HuffmanTableClass tableClass;
+        private final int tableDestinationId;
+        private final byte[] lengthBytes;
+        private final byte[] valueBytes;
+
+        public HuffmanTable (
+            @NotNull HuffmanTableClass
+            tableClass,
+            int tableDestinationId,
+            @NotNull byte[] lBytes,
+            @NotNull byte[] vBytes
+        ) {
+            this.tableClass = tableClass;
+            this.tableDestinationId = tableDestinationId;
+            this.lengthBytes = lBytes;
+            this.valueBytes = vBytes;
+            this.tableLength = vBytes.length + 17;
+        }
+
+        /**
+         * @return The table length in bytes.
+         */
+        public int getTableLength() {
+            return tableLength;
+        }
+
+
+        /**
+         * @return The {@link HuffmanTableClass} of this table.
+         */
+        public HuffmanTableClass getTableClass() {
+            return tableClass;
+        }
+
+
+        /**
+         * @return the the destination identifier for this table.
+         */
+        public int getTableDestinationId() {
+            return tableDestinationId;
+        }
+
+
+        /**
+         * @return A byte array with the L values for this table.
+         */
+        public byte[] getLengthBytes() {
+            if (lengthBytes == null)
+                return null;
+            byte[] result = new byte[lengthBytes.length];
+            System.arraycopy(lengthBytes, 0, result, 0, lengthBytes.length);
+            return result;
+        }
+
+
+        /**
+         * @return A byte array with the V values for this table.
+         */
+        public byte[] getValueBytes() {
+            if (valueBytes == null)
+                return null;
+            byte[] result = new byte[valueBytes.length];
+            System.arraycopy(valueBytes, 0, result, 0, valueBytes.length);
+            return result;
+        }
+
+        /**
+         * Evaluates whether this table is a "typical" Huffman table.
+         * <p>
+         * "Typical" has a special meaning in this context as the JPEG standard
+         * (ISO/IEC 10918 or ITU-T T.81) defines 4 Huffman tables that has been
+         * developed from the average statistics of a large set of images with
+         * 8-bit precision. Using these instead of calculating the optimal
+         * Huffman tables for a given image is faster, and is preferred by many
+         * hardware encoders and some hardware decoders.
+         * <p>
+         * Even though the JPEG standard doesn't define these as
+         * "standard tables" and requires a decoder to be able to read any valid
+         * Huffman tables, some are in reality limited decoding images using
+         * these "typical" tables. Standards like DCF (Design rule for Camera
+         * File system) and DLNA (Digital Living Network Alliance) actually
+         * requires any compliant JPEG to use only the "typical" Huffman tables.
+         * <p>
+         * This is also related to the term "optimized" JPEG. An "optimized"
+         * JPEG is a JPEG that doesn't use the "typical" Huffman tables.
+         *
+         * @return Whether or not this table is one of the predefined "typical"
+         *         Huffman tables.
+         */
+        public boolean isTypical() {
+            if (tableClass == HuffmanTableClass.DC) {
+                return
+                    Arrays.equals(lengthBytes, TYPICAL_LUMINANCE_DC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_LUMINANCE_DC_VALUES) ||
+                    Arrays.equals(lengthBytes, TYPICAL_CHROMINANCE_DC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_CHROMINANCE_DC_VALUES);
+            } else if (tableClass == HuffmanTableClass.AC) {
+                return
+                    Arrays.equals(lengthBytes, TYPICAL_LUMINANCE_AC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_LUMINANCE_AC_VALUES) ||
+                    Arrays.equals(lengthBytes, TYPICAL_CHROMINANCE_AC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_CHROMINANCE_AC_VALUES);
+            }
+            return false;
+        }
+
+        /**
+         * The opposite of {@link #isTypical()}.
+         *
+         * @return Whether or not this table is "optimized" - which means that
+         *         it isn't one of the "typical" Huffman tables.
+         */
+        public boolean isOptimized() {
+            return !isTypical();
+        }
+
+        public enum HuffmanTableClass {
+            DC,
+            AC,
+            UNKNOWN;
+
+            public static HuffmanTableClass typeOf(int value) {
+                switch (value) {
+                    case 0: return DC;
+                    case 1 : return AC;
+                    default: return UNKNOWN;
+                }
+            }
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,4 +30,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegCommentDescriptor extends TagDescriptor<JpegCommentDirectory>
 {
Index: /trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,4 +31,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegCommentDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,4 +25,5 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
 import java.util.Collections;
@@ -49,5 +50,5 @@
 
             // The entire contents of the directory are the comment
-            directory.setString(JpegCommentDirectory.TAG_COMMENT, new String(segmentBytes));
+            directory.setStringValue(JpegCommentDirectory.TAG_COMMENT, new StringValue(segmentBytes, null));
         }
     }
Index: /trunk/src/com/drew/metadata/jpeg/JpegComponent.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -89,3 +89,14 @@
         return _samplingFactorByte & 0x0F;
     }
+
+    @NotNull
+    @Override
+    public String toString() {
+        return String.format(
+            "Quantization table %d, Sampling factors %d horiz/%d vert",
+            _quantizationTableNumber,
+            getHorizontalSamplingFactor(),
+            getVerticalSamplingFactor()
+        );
+    }
 }
Index: /trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegDescriptor extends TagDescriptor<JpegDirectory>
 {
@@ -124,7 +125,5 @@
             return null;
 
-        return value.getComponentName() + " component: Quantization table " + value.getQuantizationTableNumber()
-            + ", Sampling factors " + value.getHorizontalSamplingFactor()
-            + " horiz/" + value.getVerticalSamplingFactor() + " vert";
+        return value.getComponentName() + " component: " + value;
     }
 }
Index: /trunk/src/com/drew/metadata/jpeg/JpegDhtReader.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegDhtReader.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/jpeg/JpegDhtReader.java	(revision 13061)
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2017 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.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Reader for JPEG Huffman tables, found in the DHT JPEG segment.
+ *
+ * @author Nadahar
+ */
+public class JpegDhtReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.DHT);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            extract(new SequentialByteArrayReader(segmentBytes), metadata);
+        }
+    }
+
+    /**
+     * Performs the DHT tables extraction, adding found tables to the specified
+     * instance of {@link Metadata}.
+     */
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        HuffmanTablesDirectory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class);
+        if (directory == null) {
+            directory = new HuffmanTablesDirectory();
+            metadata.addDirectory(directory);
+        }
+
+        try {
+            while (reader.available() > 0) {
+                byte header = reader.getByte();
+                HuffmanTableClass tableClass = HuffmanTableClass.typeOf((header & 0xF0) >> 4);
+                int tableDestinationId = header & 0xF;
+
+                byte[] lBytes = getBytes(reader, 16);
+                int vCount = 0;
+                for (byte b : lBytes) {
+                    vCount += (b & 0xFF);
+                }
+                byte[] vBytes = getBytes(reader, vCount);
+                directory.getTables().add(new HuffmanTable(tableClass, tableDestinationId, lBytes, vBytes));
+            }
+        } catch (IOException me) {
+            directory.addError(me.getMessage());
+        }
+
+        directory.setInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES, directory.getTables().size());
+    }
+
+    private byte[] getBytes(@NotNull final SequentialReader reader, int count) throws IOException {
+        byte[] bytes = new byte[count];
+        for (int i = 0; i < count; i++) {
+            byte b = reader.getByte();
+            if ((b & 0xFF) == 0xFF) {
+                byte stuffing = reader.getByte();
+                if (stuffing != 0x00) {
+                    throw new IOException("Marker " + JpegSegmentType.fromByte(stuffing) + " found inside DHT segment");
+                }
+            }
+            bytes[i] = b;
+        }
+        return bytes;
+    }
+}
Index: /trunk/src/com/drew/metadata/jpeg/JpegDirectory.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,4 +33,5 @@
  * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegDirectory extends Directory
 {
Index: /trunk/src/com/drew/metadata/jpeg/JpegDnlReader.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegDnlReader.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/jpeg/JpegDnlReader.java	(revision 13061)
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2017 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.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.ErrorDirectory;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Decodes JPEG DNL data, adjusting the image height with information missing from the JPEG SOFx segment.
+ *
+ * @author Nadahar
+ */
+public class JpegDnlReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.DNL);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            extract(segmentBytes, metadata, segmentType);
+        }
+    }
+
+    public void extract(byte[] segmentBytes, Metadata metadata, JpegSegmentType segmentType)
+    {
+        JpegDirectory directory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
+        if (directory == null) {
+            ErrorDirectory errorDirectory = new ErrorDirectory();
+            metadata.addDirectory(errorDirectory);
+            errorDirectory.addError("DNL segment found without SOFx - illegal JPEG format");
+            return;
+        }
+
+        SequentialReader reader = new SequentialByteArrayReader(segmentBytes);
+
+        try {
+            // Only set height from DNL if it's not already defined
+            Integer i = directory.getInteger(JpegDirectory.TAG_IMAGE_HEIGHT);
+            if (i == null || i == 0) {
+                directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
+            }
+        } catch (IOException ex) {
+            directory.addError(ex.getMessage());
+        }
+    }
+}
Index: /trunk/src/com/drew/metadata/jpeg/JpegReader.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -52,5 +52,5 @@
             JpegSegmentType.SOF6,
             JpegSegmentType.SOF7,
-            JpegSegmentType.SOF8,
+//            JpegSegmentType.JPG,
             JpegSegmentType.SOF9,
             JpegSegmentType.SOF10,
Index: /trunk/src/com/drew/metadata/jpeg/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/jpeg/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of JPEG file format metadata.
+ */
+package com.drew.metadata.jpeg;
Index: unk/src/com/drew/metadata/jpeg/package.html
===================================================================
--- /trunk/src/com/drew/metadata/jpeg/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of JPEG file format metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/package-info.java	(revision 13061)
@@ -0,0 +1,6 @@
+/**
+ * Provides classes for generic modelling of metadata directories and tags.
+ * <p />
+ * Contains base types for metadata processing abstraction.
+ */
+package com.drew.metadata;
Index: unk/src/com/drew/metadata/package.html
===================================================================
--- /trunk/src/com/drew/metadata/package.html	(revision 13060)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Provides classes for generic modelling of metadata directories and tags.  Contains base types for metadata processing abstraction.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: /trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
===================================================================
--- /trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 13060)
+++ /trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,5 +25,7 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
+import com.drew.metadata.ErrorDirectory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
 import java.util.Stack;
@@ -41,9 +43,20 @@
     protected final Metadata _metadata;
 
-    protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectoryClass)
+    protected DirectoryTiffHandler(Metadata metadata)
     {
         _metadata = metadata;
+    }
+
+    public void endingIFD()
+    {
+        _currentDirectory = _directoryStack.empty() ? null : _directoryStack.pop();
+    }
+
+    protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
+    {
+        Directory newDirectory = null;
+
         try {
-            _currentDirectory = initialDirectoryClass.newInstance();
+            newDirectory = directoryClass.newInstance();
         } catch (InstantiationException e) {
             throw new RuntimeException(e);
@@ -51,35 +64,38 @@
             throw new RuntimeException(e);
         }
-        _metadata.addDirectory(_currentDirectory);
-    }
-
-    public void endingIFD()
-    {
-        _currentDirectory = _directoryStack.empty() ? null : _directoryStack.pop();
-    }
-
-    protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
-    {
-        _directoryStack.push(_currentDirectory);
-        try {
-            Directory newDirectory = directoryClass.newInstance();
-            newDirectory.setParent(_currentDirectory);
+
+        if (newDirectory != null)
+        {
+            // If this is the first directory, don't add to the stack
+            if (_currentDirectory != null)
+            {
+                _directoryStack.push(_currentDirectory);
+                newDirectory.setParent(_currentDirectory);
+            }
             _currentDirectory = newDirectory;
-        } catch (InstantiationException e) {
-            throw new RuntimeException(e);
-        } catch (IllegalAccessException e) {
-            throw new RuntimeException(e);
+            _metadata.addDirectory(_currentDirectory);
         }
-        _metadata.addDirectory(_currentDirectory);
     }
 
     public void warn(@NotNull String message)
     {
-        _currentDirectory.addError(message);
+        getCurrentOrErrorDirectory().addError(message);
     }
 
     public void error(@NotNull String message)
     {
-        _currentDirectory.addError(message);
+        getCurrentOrErrorDirectory().addError(message);
+    }
+
+    @NotNull
+    private Directory getCurrentOrErrorDirectory()
+    {
+        if (_currentDirectory != null)
+            return _currentDirectory;
+        ErrorDirectory error = _metadata.getFirstDirectoryOfType(ErrorDirectory.class);
+        if (error != null)
+            return error;
+        pushDirectory(ErrorDirectory.class);
+        return _currentDirectory;
     }
 
@@ -89,7 +105,7 @@
     }
 
-    public void setString(int tagId, @NotNull String string)
-    {
-        _currentDirectory.setString(tagId, string);
+    public void setString(int tagId, @NotNull StringValue string)
+    {
+        _currentDirectory.setStringValue(tagId, string);
     }
 
Index: /trunk/src/com/drew/metadata/tiff/package-info.java
===================================================================
--- /trunk/src/com/drew/metadata/tiff/package-info.java	(revision 13061)
+++ /trunk/src/com/drew/metadata/tiff/package-info.java	(revision 13061)
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of TIFF file metadata.
+ *
+ * @since 2.7.0
+ */
+package com.drew.metadata.tiff;
Index: unk/src/com/drew/metadata/tiff/package.html
===================================================================
--- /trunk/src/com/drew/metadata/tiff/package.html	(revision 13060)
+++ 	(revision )
@@ -1,34 +1,0 @@
-<!--
-  ~ 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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of TIFF file metadata.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
