Index: trunk/src/com/drew/imaging/ImageProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 15216)
+++ trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,5 +35,5 @@
 //import com.drew.metadata.adobe.AdobeJpegReader;
 import com.drew.metadata.exif.ExifReader;
-import com.drew.metadata.file.FileMetadataReader;
+import com.drew.metadata.file.FileSystemMetadataReader;
 //import com.drew.metadata.icc.IccReader;
 import com.drew.metadata.iptc.IptcReader;
@@ -95,5 +95,5 @@
             inputStream.close();
         }
-        new FileMetadataReader().read(file, metadata);
+        new FileSystemMetadataReader().read(file, metadata);
         return metadata;
     }
Index: trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 15216)
+++ trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 15216)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -153,6 +153,7 @@
                 segmentData.addSegment(segmentType, segmentBytes);
             } else {
-                // Some if the JPEG is truncated, just return what data we've already gathered
+                // Skip this segment
                 if (!reader.trySkip(segmentLength)) {
+                    // If skipping failed, just return the segments we found so far
                     return segmentData;
                 }
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 15216)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentType.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/tiff/TiffDataFormat.java
===================================================================
--- trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 15216)
+++ trunk/src/com/drew/imaging/tiff/TiffDataFormat.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/imaging/tiff/TiffHandler.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/imaging/tiff/TiffProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 15216)
+++ trunk/src/com/drew/imaging/tiff/TiffProcessingException.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/imaging/tiff/TiffReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- trunk/src/com/drew/lang/BufferBoundsException.java	(revision 15216)
+++ trunk/src/com/drew/lang/BufferBoundsException.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/lang/ByteArrayReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/Charsets.java
===================================================================
--- trunk/src/com/drew/lang/Charsets.java	(revision 15216)
+++ trunk/src/com/drew/lang/Charsets.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -38,3 +38,4 @@
     public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
     public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
+    public static final Charset WINDOWS_1252 = Charset.forName("Cp1252");
 }
Index: trunk/src/com/drew/lang/CompoundException.java
===================================================================
--- trunk/src/com/drew/lang/CompoundException.java	(revision 15216)
+++ trunk/src/com/drew/lang/CompoundException.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/lang/GeoLocation.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/lang/RandomAccessReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,11 +22,11 @@
 package com.drew.lang;
 
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
+
 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;
 
 /**
Index: trunk/src/com/drew/lang/Rational.java
===================================================================
--- trunk/src/com/drew/lang/Rational.java	(revision 15216)
+++ trunk/src/com/drew/lang/Rational.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/SequentialByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 15216)
+++ trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/SequentialReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialReader.java	(revision 15216)
+++ trunk/src/com/drew/lang/SequentialReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/StreamReader.java
===================================================================
--- trunk/src/com/drew/lang/StreamReader.java	(revision 15216)
+++ trunk/src/com/drew/lang/StreamReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- trunk/src/com/drew/lang/StringUtil.java	(revision 15216)
+++ trunk/src/com/drew/lang/StringUtil.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- trunk/src/com/drew/lang/annotations/NotNull.java	(revision 15216)
+++ trunk/src/com/drew/lang/annotations/NotNull.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/lang/annotations/Nullable.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Age.java
===================================================================
--- trunk/src/com/drew/metadata/Age.java	(revision 15216)
+++ trunk/src/com/drew/metadata/Age.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Directory.java
===================================================================
--- trunk/src/com/drew/metadata/Directory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/Directory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -757,4 +757,6 @@
         if (o == null)
             return null;
+        if (o instanceof Number)
+            return ((Number)o).longValue();
         if (o instanceof String || o instanceof StringValue) {
             try {
@@ -763,7 +765,21 @@
                 return null;
             }
-        }
-        if (o instanceof Number)
-            return ((Number)o).longValue();
+        } else if (o instanceof Rational[]) {
+            Rational[] rationals = (Rational[])o;
+            if (rationals.length == 1)
+                return rationals[0].longValue();
+        } else if (o instanceof byte[]) {
+            byte[] bytes = (byte[])o;
+            if (bytes.length == 1)
+                return (long)bytes[0];
+        } else if (o instanceof int[]) {
+            int[] ints = (int[])o;
+            if (ints.length == 1)
+                return (long)ints[0];
+        } else if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            if (shorts.length == 1)
+                return (long)shorts[0];
+        }
         return null;
     }
@@ -1009,14 +1025,18 @@
                 }
             } else if (componentType.getName().equals("float")) {
+                DecimalFormat format = new DecimalFormat(_floatFormatPattern);
                 for (int i = 0; i < arrayLength; i++) {
                     if (i != 0)
                         string.append(' ');
-                    string.append(new DecimalFormat(_floatFormatPattern).format(Array.getFloat(o, i)));
+                    String s = format.format(Array.getFloat(o, i));
+                    string.append(s.equals("-0") ? "0" : s);
                 }
             } else if (componentType.getName().equals("double")) {
+                DecimalFormat format = new DecimalFormat(_floatFormatPattern);
                 for (int i = 0; i < arrayLength; i++) {
                     if (i != 0)
                         string.append(' ');
-                    string.append(new DecimalFormat(_floatFormatPattern).format(Array.getDouble(o, i)));
+                    String s = format.format(Array.getDouble(o, i));
+                    string.append(s.equals("-0") ? "0" : s);
                 }
             } else if (componentType.getName().equals("byte")) {
Index: trunk/src/com/drew/metadata/ErrorDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/ErrorDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/ErrorDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Face.java
===================================================================
--- trunk/src/com/drew/metadata/Face.java	(revision 15216)
+++ trunk/src/com/drew/metadata/Face.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/metadata/Metadata.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/MetadataException.java
===================================================================
--- trunk/src/com/drew/metadata/MetadataException.java	(revision 15216)
+++ trunk/src/com/drew/metadata/MetadataException.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    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 15216)
+++ trunk/src/com/drew/metadata/StringValue.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/Tag.java
===================================================================
--- trunk/src/com/drew/metadata/Tag.java	(revision 15216)
+++ trunk/src/com/drew/metadata/Tag.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/TagDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/TagDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/TagDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,5 +34,7 @@
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
@@ -79,6 +81,5 @@
         }
 
-        if (object instanceof Date)
-        {
+        if (object instanceof Date) {
             // Produce a date string having a format that includes the offset in form "+00:00"
             return new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy")
@@ -140,10 +141,10 @@
     protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions)
     {
-        final Integer index = _directory.getInteger(tagType);
+        final Long index = _directory.getLongObject(tagType);
         if (index == null)
             return null;
-        final int arrayIndex = index - baseIndex;
+        final long arrayIndex = index - baseIndex;
         if (arrayIndex >= 0 && arrayIndex < descriptions.length) {
-            String description = descriptions[arrayIndex];
+            String description = descriptions[(int)arrayIndex];
             if (description != null)
                 return description;
@@ -211,5 +212,5 @@
         // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
         Long value = _directory.getLongObject(tagType);
-        if (value==null)
+        if (value == null)
             return null;
         return new Date(value).toString();
@@ -227,5 +228,5 @@
             return null;
 
-        List<String> parts = new ArrayList<String>();
+        List<String> parts = new ArrayList<>();
 
         int bitIndex = 0;
@@ -292,6 +293,5 @@
 
         Double d = _directory.getDoubleObject(tagType);
-        if (d != null)
-        {
+        if (d != null) {
             DecimalFormat format = new DecimalFormat("0.###");
             return format.format(d);
@@ -379,6 +379,6 @@
         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;
+            long apexPower10 = Math.round(apexPower * 10.0);
+            float fApexPower = apexPower10 / 10.0f;
             DecimalFormat format = new DecimalFormat("0.##");
             format.setRoundingMode(RoundingMode.HALF_UP);
@@ -414,6 +414,5 @@
     protected String getLightSourceDescription(short wbtype)
     {
-        switch (wbtype)
-        {
+        switch (wbtype) {
             case 0:
                 return "Unknown";
@@ -464,3 +463,44 @@
         return getDescription(wbtype);
     }
+
+    // EXIF UserComment, GPSProcessingMethod and GPSAreaInformation
+    @Nullable
+    protected String getEncodedTextDescription(int tagType)
+    {
+        byte[] commentBytes = _directory.getByteArray(tagType);
+        if (commentBytes == null)
+            return null;
+        if (commentBytes.length == 0)
+            return "";
+
+        final Map<String, String> encodingMap = new HashMap<>();
+        encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
+        encodingMap.put("UNICODE", "UTF-16LE");
+        encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
+
+        try {
+            if (commentBytes.length >= 10) {
+                String firstTenBytesString = new String(commentBytes, 0, 10);
+
+                // try each encoding name
+                for (Map.Entry<String, String> pair : encodingMap.entrySet()) {
+                    String encodingName = pair.getKey();
+                    String charset = pair.getValue();
+                    if (firstTenBytesString.startsWith(encodingName)) {
+                        // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start
+                        for (int j = encodingName.length(); j < 10; j++) {
+                            byte b = commentBytes[j];
+                            if (b != '\0' && b != ' ')
+                                return new String(commentBytes, j, commentBytes.length - j, charset).trim();
+                        }
+                        return new String(commentBytes, 10, commentBytes.length - 10, charset).trim();
+                    }
+                }
+            }
+            // special handling fell through, return a plain string representation
+            return new String(commentBytes, System.getProperty("file.encoding")).trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,6 +33,4 @@
 import java.io.UnsupportedEncodingException;
 import java.text.DecimalFormat;
-import java.util.HashMap;
-import java.util.Map;
 
 import static com.drew.metadata.exif.ExifDirectoryBase.*;
@@ -67,6 +65,4 @@
     public String getDescription(int tagType)
     {
-        // TODO order case blocks and corresponding methods in the same order as the TAG_* values are defined
-
         switch (tagType) {
             case TAG_INTEROP_INDEX:
@@ -74,14 +70,8 @@
             case TAG_INTEROP_VERSION:
                 return getInteropVersionDescription();
-            case TAG_ORIENTATION:
-                return getOrientationDescription();
-            case TAG_RESOLUTION_UNIT:
-                return getResolutionDescription();
-            case TAG_YCBCR_POSITIONING:
-                return getYCbCrPositioningDescription();
-            case TAG_X_RESOLUTION:
-                return getXResolutionDescription();
-            case TAG_Y_RESOLUTION:
-                return getYResolutionDescription();
+            case TAG_NEW_SUBFILE_TYPE:
+                return getNewSubfileTypeDescription();
+            case TAG_SUBFILE_TYPE:
+                return getSubfileTypeDescription();
             case TAG_IMAGE_WIDTH:
                 return getImageWidthDescription();
@@ -90,46 +80,64 @@
             case TAG_BITS_PER_SAMPLE:
                 return getBitsPerSampleDescription();
+            case TAG_COMPRESSION:
+                return getCompressionDescription();
             case TAG_PHOTOMETRIC_INTERPRETATION:
                 return getPhotometricInterpretationDescription();
+            case TAG_THRESHOLDING:
+                return getThresholdingDescription();
+            case TAG_FILL_ORDER:
+                return getFillOrderDescription();
+            case TAG_ORIENTATION:
+                return getOrientationDescription();
+            case TAG_SAMPLES_PER_PIXEL:
+                return getSamplesPerPixelDescription();
             case TAG_ROWS_PER_STRIP:
                 return getRowsPerStripDescription();
             case TAG_STRIP_BYTE_COUNTS:
                 return getStripByteCountsDescription();
-            case TAG_SAMPLES_PER_PIXEL:
-                return getSamplesPerPixelDescription();
+            case TAG_X_RESOLUTION:
+                return getXResolutionDescription();
+            case TAG_Y_RESOLUTION:
+                return getYResolutionDescription();
             case TAG_PLANAR_CONFIGURATION:
                 return getPlanarConfigurationDescription();
+            case TAG_RESOLUTION_UNIT:
+                return getResolutionDescription();
+            case TAG_JPEG_PROC:
+                return getJpegProcDescription();
             case TAG_YCBCR_SUBSAMPLING:
                 return getYCbCrSubsamplingDescription();
+            case TAG_YCBCR_POSITIONING:
+                return getYCbCrPositioningDescription();
             case TAG_REFERENCE_BLACK_WHITE:
                 return getReferenceBlackWhiteDescription();
-            case TAG_WIN_AUTHOR:
-                return getWindowsAuthorDescription();
-            case TAG_WIN_COMMENT:
-                return getWindowsCommentDescription();
-            case TAG_WIN_KEYWORDS:
-                return getWindowsKeywordsDescription();
-            case TAG_WIN_SUBJECT:
-                return getWindowsSubjectDescription();
-            case TAG_WIN_TITLE:
-                return getWindowsTitleDescription();
-            case TAG_NEW_SUBFILE_TYPE:
-                return getNewSubfileTypeDescription();
-            case TAG_SUBFILE_TYPE:
-                return getSubfileTypeDescription();
-            case TAG_THRESHOLDING:
-                return getThresholdingDescription();
-            case TAG_FILL_ORDER:
-                return getFillOrderDescription();
             case TAG_CFA_PATTERN_2:
                 return getCfaPattern2Description();
             case TAG_EXPOSURE_TIME:
                 return getExposureTimeDescription();
+            case TAG_FNUMBER:
+                return getFNumberDescription();
+            case TAG_EXPOSURE_PROGRAM:
+                return getExposureProgramDescription();
+            case TAG_ISO_EQUIVALENT:
+                return getIsoEquivalentDescription();
+            case TAG_SENSITIVITY_TYPE:
+                return getSensitivityTypeRangeDescription();
+            case TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case TAG_COMPONENTS_CONFIGURATION:
+                return getComponentConfigurationDescription();
+            case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
+                return getCompressedAverageBitsPerPixelDescription();
             case TAG_SHUTTER_SPEED:
                 return getShutterSpeedDescription();
-            case TAG_FNUMBER:
-                return getFNumberDescription();
-            case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
-                return getCompressedAverageBitsPerPixelDescription();
+            case TAG_APERTURE:
+                return getApertureValueDescription();
+            case TAG_BRIGHTNESS_VALUE:
+                return getBrightnessValueDescription();
+            case TAG_EXPOSURE_BIAS:
+                return getExposureBiasDescription();
+            case TAG_MAX_APERTURE:
+                return getMaxApertureValueDescription();
             case TAG_SUBJECT_DISTANCE:
                 return getSubjectDistanceDescription();
@@ -142,4 +150,30 @@
             case TAG_FOCAL_LENGTH:
                 return getFocalLengthDescription();
+            case TAG_USER_COMMENT:
+                return getUserCommentDescription();
+            case TAG_TEMPERATURE:
+                return getTemperatureDescription();
+            case TAG_HUMIDITY:
+                return getHumidityDescription();
+            case TAG_PRESSURE:
+                return getPressureDescription();
+            case TAG_WATER_DEPTH:
+                return getWaterDepthDescription();
+            case TAG_ACCELERATION:
+                return getAccelerationDescription();
+            case TAG_CAMERA_ELEVATION_ANGLE:
+                return getCameraElevationAngleDescription();
+            case TAG_WIN_TITLE:
+                return getWindowsTitleDescription();
+            case TAG_WIN_COMMENT:
+                return getWindowsCommentDescription();
+            case TAG_WIN_AUTHOR:
+                return getWindowsAuthorDescription();
+            case TAG_WIN_KEYWORDS:
+                return getWindowsKeywordsDescription();
+            case TAG_WIN_SUBJECT:
+                return getWindowsSubjectDescription();
+            case TAG_FLASHPIX_VERSION:
+                return getFlashPixVersionDescription();
             case TAG_COLOR_SPACE:
                 return getColorSpaceDescription();
@@ -148,20 +182,12 @@
             case TAG_EXIF_IMAGE_HEIGHT:
                 return getExifImageHeightDescription();
-            case TAG_FOCAL_PLANE_RESOLUTION_UNIT:
-                return getFocalPlaneResolutionUnitDescription();
             case TAG_FOCAL_PLANE_X_RESOLUTION:
                 return getFocalPlaneXResolutionDescription();
             case TAG_FOCAL_PLANE_Y_RESOLUTION:
                 return getFocalPlaneYResolutionDescription();
-            case TAG_EXPOSURE_PROGRAM:
-                return getExposureProgramDescription();
-            case TAG_APERTURE:
-                return getApertureValueDescription();
-            case TAG_MAX_APERTURE:
-                return getMaxApertureValueDescription();
+            case TAG_FOCAL_PLANE_RESOLUTION_UNIT:
+                return getFocalPlaneResolutionUnitDescription();
             case TAG_SENSING_METHOD:
                 return getSensingMethodDescription();
-            case TAG_EXPOSURE_BIAS:
-                return getExposureBiasDescription();
             case TAG_FILE_SOURCE:
                 return getFileSourceDescription();
@@ -170,14 +196,4 @@
             case TAG_CFA_PATTERN:
                 return getCfaPatternDescription();
-            case TAG_COMPONENTS_CONFIGURATION:
-                return getComponentConfigurationDescription();
-            case TAG_EXIF_VERSION:
-                return getExifVersionDescription();
-            case TAG_FLASHPIX_VERSION:
-                return getFlashPixVersionDescription();
-            case TAG_ISO_EQUIVALENT:
-                return getIsoEquivalentDescription();
-            case TAG_USER_COMMENT:
-                return getUserCommentDescription();
             case TAG_CUSTOM_RENDERED:
                 return getCustomRenderedDescription();
@@ -202,10 +218,4 @@
             case TAG_SUBJECT_DISTANCE_RANGE:
                 return getSubjectDistanceRangeDescription();
-            case TAG_SENSITIVITY_TYPE:
-                return getSensitivityTypeRangeDescription();
-            case TAG_COMPRESSION:
-                return getCompressionDescription();
-            case TAG_JPEG_PROC:
-                return getJpegProcDescription();
             case TAG_LENS_SPECIFICATION:
                 return getLensSpecificationDescription();
@@ -216,10 +226,4 @@
 
     @Nullable
-    public String getInteropVersionDescription()
-    {
-        return getVersionBytesDescription(TAG_INTEROP_VERSION, 2);
-    }
-
-    @Nullable
     public String getInteropIndexDescription()
     {
@@ -235,219 +239,7 @@
 
     @Nullable
-    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)
-        {
-            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];
-        int blackG = ints[2];
-        int whiteG = ints[3];
-        int blackB = ints[4];
-        int whiteB = ints[5];
-        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
-    }
-
-    @Nullable
-    public String getYResolutionDescription()
-    {
-        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
-        if (value==null)
-            return null;
-        final String unit = getResolutionDescription();
-        return String.format("%s dots per %s",
-            value.toSimpleString(_allowDecimalRepresentationOfRationals),
-            unit == null ? "unit" : unit.toLowerCase());
-    }
-
-    @Nullable
-    public String getXResolutionDescription()
-    {
-        Rational value = _directory.getRational(TAG_X_RESOLUTION);
-        if (value == null)
-            return null;
-        final String unit = getResolutionDescription();
-        return String.format("%s dots per %s",
-            value.toSimpleString(_allowDecimalRepresentationOfRationals),
-            unit == null ? "unit" : unit.toLowerCase());
-    }
-
-    @Nullable
-    public String getYCbCrPositioningDescription()
-    {
-        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
-    }
-
-    @Nullable
-    public String getOrientationDescription()
-    {
-        return super.getOrientationDescription(TAG_ORIENTATION);
-    }
-
-    @Nullable
-    public String getResolutionDescription()
-    {
-        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
-        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
-    }
-
-    /** The Windows specific tags uses plain Unicode. */
-    @Nullable
-    private String getUnicodeDescription(int tag)
-    {
-        byte[] bytes = _directory.getByteArray(tag);
-        if (bytes == null)
-            return null;
-        try {
-            // Decode the unicode string and trim the unicode zero "\0" from the end.
-            return new String(bytes, "UTF-16LE").trim();
-        } catch (UnsupportedEncodingException ex) {
-            return null;
-        }
-    }
-
-    @Nullable
-    public String getWindowsAuthorDescription()
-    {
-        return getUnicodeDescription(TAG_WIN_AUTHOR);
-    }
-
-    @Nullable
-    public String getWindowsCommentDescription()
-    {
-        return getUnicodeDescription(TAG_WIN_COMMENT);
-    }
-
-    @Nullable
-    public String getWindowsKeywordsDescription()
-    {
-        return getUnicodeDescription(TAG_WIN_KEYWORDS);
-    }
-
-    @Nullable
-    public String getWindowsTitleDescription()
-    {
-        return getUnicodeDescription(TAG_WIN_TITLE);
-    }
-
-    @Nullable
-    public String getWindowsSubjectDescription()
-    {
-        return getUnicodeDescription(TAG_WIN_SUBJECT);
-    }
-
-    @Nullable
-    public String getYCbCrSubsamplingDescription()
-    {
-        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
-        if (positions == null || positions.length < 2)
-            return null;
-        if (positions[0] == 2 && positions[1] == 1) {
-            return "YCbCr4:2:2";
-        } else if (positions[0] == 2 && positions[1] == 2) {
-            return "YCbCr4:2:0";
-        } else {
-            return "(Unknown)";
-        }
-    }
-
-    @Nullable
-    public String getPlanarConfigurationDescription()
-    {
-        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
-        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
-        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
-        // plane format.
-        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
-            1,
-            "Chunky (contiguous for each subsampling pixel)",
-            "Separate (Y-plane/Cb-plane/Cr-plane format)"
-        );
-    }
-
-    @Nullable
-    public String getSamplesPerPixelDescription()
-    {
-        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
-        return value == null ? null : value + " samples/pixel";
-    }
-
-    @Nullable
-    public String getRowsPerStripDescription()
-    {
-        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
-        return value == null ? null : value + " rows/strip";
-    }
-
-    @Nullable
-    public String getStripByteCountsDescription()
-    {
-        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
-        return value == null ? null : value + " bytes";
-    }
-
-    @Nullable
-    public String getPhotometricInterpretationDescription()
-    {
-        // Shows the color space of the image data components
-        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 0: return "WhiteIsZero";
-            case 1: return "BlackIsZero";
-            case 2: return "RGB";
-            case 3: return "RGB Palette";
-            case 4: return "Transparency Mask";
-            case 5: return "CMYK";
-            case 6: return "YCbCr";
-            case 8: return "CIELab";
-            case 9: return "ICCLab";
-            case 10: return "ITULab";
-            case 32803: return "Color Filter Array";
-            case 32844: return "Pixar LogL";
-            case 32845: return "Pixar LogLuv";
-            case 32892: return "Linear Raw";
-            default:
-                return "Unknown colour space";
-        }
-    }
-
-    @Nullable
-    public String getBitsPerSampleDescription()
-    {
-        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
-        return value == null ? null : value + " bits/component/pixel";
-    }
-
-    @Nullable
-    public String getImageWidthDescription()
-    {
-        String value = _directory.getString(TAG_IMAGE_WIDTH);
-        return value == null ? null : value + " pixels";
-    }
-
-    @Nullable
-    public String getImageHeightDescription()
-    {
-        String value = _directory.getString(TAG_IMAGE_HEIGHT);
-        return value == null ? null : value + " pixels";
+    public String getInteropVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_INTEROP_VERSION, 2);
     }
 
@@ -478,610 +270,22 @@
 
     @Nullable
-    public String getThresholdingDescription()
-    {
-        return getIndexedDescription(TAG_THRESHOLDING, 1,
-            "No dithering or halftoning",
-            "Ordered dither or halftone",
-            "Randomized dither"
-        );
-    }
-
-    @Nullable
-    public String getFillOrderDescription()
-    {
-        return getIndexedDescription(TAG_FILL_ORDER, 1,
-            "Normal",
-            "Reversed"
-        );
-    }
-
-    @Nullable
-    public String getSubjectDistanceRangeDescription()
-    {
-        return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE,
-            "Unknown",
-            "Macro",
-            "Close view",
-            "Distant view"
-        );
-    }
-
-    @Nullable
-    public String getSensitivityTypeRangeDescription()
-    {
-        return getIndexedDescription(TAG_SENSITIVITY_TYPE,
-            "Unknown",
-            "Standard Output Sensitivity",
-            "Recommended Exposure Index",
-            "ISO Speed",
-            "Standard Output Sensitivity and Recommended Exposure Index",
-            "Standard Output Sensitivity and ISO Speed",
-            "Recommended Exposure Index and ISO Speed",
-            "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed"
-        );
-    }
-
-    @Nullable
-    public String getLensSpecificationDescription()
-    {
-        return getLensSpecificationDescription(TAG_LENS_SPECIFICATION);
-    }
-
-    @Nullable
-    public String getSharpnessDescription()
-    {
-        return getIndexedDescription(TAG_SHARPNESS,
-            "None",
-            "Low",
-            "Hard"
-        );
-    }
-
-    @Nullable
-    public String getSaturationDescription()
-    {
-        return getIndexedDescription(TAG_SATURATION,
-            "None",
-            "Low saturation",
-            "High saturation"
-        );
-    }
-
-    @Nullable
-    public String getContrastDescription()
-    {
-        return getIndexedDescription(TAG_CONTRAST,
-            "None",
-            "Soft",
-            "Hard"
-        );
-    }
-
-    @Nullable
-    public String getGainControlDescription()
-    {
-        return getIndexedDescription(TAG_GAIN_CONTROL,
-            "None",
-            "Low gain up",
-            "Low gain down",
-            "High gain up",
-            "High gain down"
-        );
-    }
-
-    @Nullable
-    public String getSceneCaptureTypeDescription()
-    {
-        return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE,
-            "Standard",
-            "Landscape",
-            "Portrait",
-            "Night scene"
-        );
-    }
-
-    @Nullable
-    public String get35mmFilmEquivFocalLengthDescription()
-    {
-        Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
-        return value == null
-            ? null
-            : value == 0
-                ? "Unknown"
-                : getFocalLengthDescription(value);
-    }
-
-    @Nullable
-    public String getDigitalZoomRatioDescription()
-    {
-        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
-        return value == null
-            ? null
-            : value.getNumerator() == 0
-                ? "Digital zoom not used"
-                : new DecimalFormat("0.#").format(value.doubleValue());
-    }
-
-    @Nullable
-    public String getWhiteBalanceModeDescription()
-    {
-        return getIndexedDescription(TAG_WHITE_BALANCE_MODE,
-            "Auto white balance",
-            "Manual white balance"
-        );
-    }
-
-    @Nullable
-    public String getExposureModeDescription()
-    {
-        return getIndexedDescription(TAG_EXPOSURE_MODE,
-            "Auto exposure",
-            "Manual exposure",
-            "Auto bracket"
-        );
-    }
-
-    @Nullable
-    public String getCustomRenderedDescription()
-    {
-        return getIndexedDescription(TAG_CUSTOM_RENDERED,
-            "Normal process",
-            "Custom process"
-        );
-    }
-
-    @Nullable
-    public String getUserCommentDescription()
-    {
-        byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT);
-        if (commentBytes == null)
-            return null;
-        if (commentBytes.length == 0)
-            return "";
-
-        final Map<String, String> encodingMap = new HashMap<String, String>();
-        encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
-        encodingMap.put("UNICODE", "UTF-16LE");
-        encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
-
-        try {
-            if (commentBytes.length >= 10) {
-                String firstTenBytesString = new String(commentBytes, 0, 10);
-
-                // try each encoding name
-                for (Map.Entry<String, String> pair : encodingMap.entrySet()) {
-                    String encodingName = pair.getKey();
-                    String charset = pair.getValue();
-                    if (firstTenBytesString.startsWith(encodingName)) {
-                        // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start
-                        for (int j = encodingName.length(); j < 10; j++) {
-                            byte b = commentBytes[j];
-                            if (b != '\0' && b != ' ')
-                                return new String(commentBytes, j, commentBytes.length - j, charset).trim();
-                        }
-                        return new String(commentBytes, 10, commentBytes.length - 10, charset).trim();
-                    }
-                }
-            }
-            // special handling fell through, return a plain string representation
-            return new String(commentBytes, System.getProperty("file.encoding")).trim();
-        } catch (UnsupportedEncodingException ex) {
-            return null;
-        }
-    }
-
-    @Nullable
-    public String getIsoEquivalentDescription()
-    {
-        // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values
-        Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT);
-        // There used to be a check here that multiplied ISO values < 50 by 200.
-        // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40.
-        return isoEquiv != null
-            ? Integer.toString(isoEquiv)
-            : null;
-    }
-
-    @Nullable
-    public String getExifVersionDescription()
-    {
-        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
-    }
-
-    @Nullable
-    public String getFlashPixVersionDescription()
-    {
-        return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2);
-    }
-
-    @Nullable
-    public String getSceneTypeDescription()
-    {
-        return getIndexedDescription(TAG_SCENE_TYPE,
-            1,
-            "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;
-    }
-
-    @Nullable
-    public String getFileSourceDescription()
-    {
-        return getIndexedDescription(TAG_FILE_SOURCE,
-            1,
-            "Film Scanner",
-            "Reflection Print Scanner",
-            "Digital Still Camera (DSC)"
-        );
-    }
-
-    @Nullable
-    public String getExposureBiasDescription()
-    {
-        Rational value = _directory.getRational(TAG_EXPOSURE_BIAS);
-        if (value == null)
-            return null;
-        return value.toSimpleString(true) + " EV";
-    }
-
-    @Nullable
-    public String getMaxApertureValueDescription()
-    {
-        Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE);
-        if (aperture == null)
-            return null;
-        double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return getFStopDescription(fStop);
-    }
-
-    @Nullable
-    public String getApertureValueDescription()
-    {
-        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
-        if (aperture == null)
-            return null;
-        double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return getFStopDescription(fStop);
-    }
-
-    @Nullable
-    public String getExposureProgramDescription()
-    {
-        return getIndexedDescription(TAG_EXPOSURE_PROGRAM,
-            1,
-            "Manual control",
-            "Program normal",
-            "Aperture priority",
-            "Shutter priority",
-            "Program creative (slow program)",
-            "Program action (high-speed program)",
-            "Portrait mode",
-            "Landscape mode"
-        );
-    }
-
-
-    @Nullable
-    public String getFocalPlaneXResolutionDescription()
-    {
-        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION);
-        if (rational == null)
-            return null;
-        final String unit = getFocalPlaneResolutionUnitDescription();
-        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
-            + (unit == null ? "" : " " + unit.toLowerCase());
-    }
-
-    @Nullable
-    public String getFocalPlaneYResolutionDescription()
-    {
-        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION);
-        if (rational == null)
-            return null;
-        final String unit = getFocalPlaneResolutionUnitDescription();
-        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
-            + (unit == null ? "" : " " + unit.toLowerCase());
-    }
-
-    @Nullable
-    public String getFocalPlaneResolutionUnitDescription()
-    {
-        // Unit of FocalPlaneXResolution/FocalPlaneYResolution.
-        // '1' means no-unit, '2' inch, '3' centimeter.
-        return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
-            1,
-            "(No unit)",
-            "Inches",
-            "cm"
-        );
-    }
-
-    @Nullable
-    public String getExifImageWidthDescription()
-    {
-        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH);
+    public String getImageWidthDescription()
+    {
+        String value = _directory.getString(TAG_IMAGE_WIDTH);
         return value == null ? null : value + " pixels";
     }
 
     @Nullable
-    public String getExifImageHeightDescription()
-    {
-        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT);
+    public String getImageHeightDescription()
+    {
+        String value = _directory.getString(TAG_IMAGE_HEIGHT);
         return value == null ? null : value + " pixels";
     }
 
     @Nullable
-    public String getColorSpaceDescription()
-    {
-        final Integer value = _directory.getInteger(TAG_COLOR_SPACE);
-        if (value == null)
-            return null;
-        if (value == 1)
-            return "sRGB";
-        if (value == 65535)
-            return "Undefined";
-        return "Unknown (" + value + ")";
-    }
-
-    @Nullable
-    public String getFocalLengthDescription()
-    {
-        Rational value = _directory.getRational(TAG_FOCAL_LENGTH);
-        return value == null ? null : getFocalLengthDescription(value.doubleValue());
-    }
-
-    @Nullable
-    public String getFlashDescription()
-    {
-        /*
-         * This is a bit mask.
-         * 0 = flash fired
-         * 1 = return detected
-         * 2 = return able to be detected
-         * 3 = unknown
-         * 4 = auto used
-         * 5 = unknown
-         * 6 = red eye reduction used
-         */
-
-        final Integer value = _directory.getInteger(TAG_FLASH);
-
-        if (value == null)
-            return null;
-
-        StringBuilder sb = new StringBuilder();
-
-        if ((value & 0x1) != 0)
-            sb.append("Flash fired");
-        else
-            sb.append("Flash did not fire");
-
-        // check if we're able to detect a return, before we mention it
-        if ((value & 0x4) != 0) {
-            if ((value & 0x2) != 0)
-                sb.append(", return detected");
-            else
-                sb.append(", return not detected");
-        }
-
-        if ((value & 0x10) != 0)
-            sb.append(", auto");
-
-        if ((value & 0x40) != 0)
-            sb.append(", red-eye reduction");
-
-        return sb.toString();
-    }
-
-    @Nullable
-    public String getWhiteBalanceDescription()
-    {
-        // See http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
-        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 0: return "Unknown";
-            case 1: return "Daylight";
-            case 2: return "Florescent";
-            case 3: return "Tungsten";
-            case 4: return "Flash";
-            case 9: return "Fine Weather";
-            case 10: return "Cloudy";
-            case 11: return "Shade";
-            case 12: return "Daylight Fluorescent";
-            case 13: return "Day White Fluorescent";
-            case 14: return "Cool White Fluorescent";
-            case 15: return "White Fluorescent";
-            case 16: return "Warm White Fluorescent";
-            case 17: return "Standard light";
-            case 18: return "Standard light (B)";
-            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 "Studio Tungsten";
-            case 255: return "(Other)";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    @Nullable
-    public String getMeteringModeDescription()
-    {
-        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
-        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
-        Integer value = _directory.getInteger(TAG_METERING_MODE);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 0: return "Unknown";
-            case 1: return "Average";
-            case 2: return "Center weighted average";
-            case 3: return "Spot";
-            case 4: return "Multi-spot";
-            case 5: return "Multi-segment";
-            case 6: return "Partial";
-            case 255: return "(Other)";
-            default:
-                return "Unknown (" + value + ")";
-        }
+    public String getBitsPerSampleDescription()
+    {
+        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
+        return value == null ? null : value + " bits/component/pixel";
     }
 
@@ -1140,11 +344,339 @@
 
     @Nullable
-    public String getSubjectDistanceDescription()
-    {
-        Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE);
-        if (value == null)
-            return null;
-        DecimalFormat formatter = new DecimalFormat("0.0##");
-        return formatter.format(value.doubleValue()) + " metres";
+    public String getPhotometricInterpretationDescription()
+    {
+        // Shows the color space of the image data components
+        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "WhiteIsZero";
+            case 1: return "BlackIsZero";
+            case 2: return "RGB";
+            case 3: return "RGB Palette";
+            case 4: return "Transparency Mask";
+            case 5: return "CMYK";
+            case 6: return "YCbCr";
+            case 8: return "CIELab";
+            case 9: return "ICCLab";
+            case 10: return "ITULab";
+            case 32803: return "Color Filter Array";
+            case 32844: return "Pixar LogL";
+            case 32845: return "Pixar LogLuv";
+            case 32892: return "Linear Raw";
+            default:
+                return "Unknown colour space";
+        }
+    }
+
+    @Nullable
+    public String getThresholdingDescription()
+    {
+        return getIndexedDescription(TAG_THRESHOLDING, 1,
+            "No dithering or halftoning",
+            "Ordered dither or halftone",
+            "Randomized dither"
+        );
+    }
+
+    @Nullable
+    public String getFillOrderDescription()
+    {
+        return getIndexedDescription(TAG_FILL_ORDER, 1,
+            "Normal",
+            "Reversed"
+        );
+    }
+
+    @Nullable
+    public String getOrientationDescription()
+    {
+        return super.getOrientationDescription(TAG_ORIENTATION);
+    }
+
+    @Nullable
+    public String getSamplesPerPixelDescription()
+    {
+        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
+        return value == null ? null : value + " samples/pixel";
+    }
+
+    @Nullable
+    public String getRowsPerStripDescription()
+    {
+        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
+        return value == null ? null : value + " rows/strip";
+    }
+
+    @Nullable
+    public String getStripByteCountsDescription()
+    {
+        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
+        return value == null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getXResolutionDescription()
+    {
+        Rational value = _directory.getRational(TAG_X_RESOLUTION);
+        if (value == null)
+            return null;
+        final String unit = getResolutionDescription();
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getYResolutionDescription()
+    {
+        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getPlanarConfigurationDescription()
+    {
+        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
+        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
+        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
+        // plane format.
+        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
+            1,
+            "Chunky (contiguous for each subsampling pixel)",
+            "Separate (Y-plane/Cb-plane/Cr-plane format)"
+        );
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
+    }
+
+    @Nullable
+    public String getJpegProcDescription()
+    {
+        Integer value = _directory.getInteger(TAG_JPEG_PROC);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Baseline";
+            case 14: return "Lossless";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getYCbCrSubsamplingDescription()
+    {
+        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
+        if (positions == null || positions.length < 2)
+            return null;
+        if (positions[0] == 2 && positions[1] == 1) {
+            return "YCbCr4:2:2";
+        } else if (positions[0] == 2 && positions[1] == 2) {
+            return "YCbCr4:2:0";
+        } else {
+            return "(Unknown)";
+        }
+    }
+
+    @Nullable
+    public String getYCbCrPositioningDescription()
+    {
+        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
+    }
+
+    @Nullable
+    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)
+        {
+            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];
+        int blackG = ints[2];
+        int whiteG = ints[3];
+        int blackB = ints[4];
+        int whiteB = ints[5];
+        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
+    }
+
+    /// <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();
+    }
+
+    @Nullable
+    public String getExposureTimeDescription()
+    {
+        String value = _directory.getString(TAG_EXPOSURE_TIME);
+        return value == null ? null : value + " sec";
+    }
+
+    @Nullable
+    public String getFNumberDescription()
+    {
+        Rational value = _directory.getRational(TAG_FNUMBER);
+        if (value == null)
+            return null;
+        return getFStopDescription(value.doubleValue());
+    }
+
+    @Nullable
+    public String getExposureProgramDescription()
+    {
+        return getIndexedDescription(TAG_EXPOSURE_PROGRAM,
+            1,
+            "Manual control",
+            "Program normal",
+            "Aperture priority",
+            "Shutter priority",
+            "Program creative (slow program)",
+            "Program action (high-speed program)",
+            "Portrait mode",
+            "Landscape mode"
+        );
+    }
+
+    @Nullable
+    public String getIsoEquivalentDescription()
+    {
+        // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values
+        Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT);
+        // There used to be a check here that multiplied ISO values < 50 by 200.
+        // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40.
+        return isoEquiv != null
+            ? Integer.toString(isoEquiv)
+            : null;
+    }
+
+    @Nullable
+    public String getSensitivityTypeRangeDescription()
+    {
+        return getIndexedDescription(TAG_SENSITIVITY_TYPE,
+            "Unknown",
+            "Standard Output Sensitivity",
+            "Recommended Exposure Index",
+            "ISO Speed",
+            "Standard Output Sensitivity and Recommended Exposure Index",
+            "Standard Output Sensitivity and ISO Speed",
+            "Recommended Exposure Index and ISO Speed",
+            "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed"
+        );
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
+    }
+
+    @Nullable
+    public String getComponentConfigurationDescription()
+    {
+        int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION);
+        if (components == null)
+            return null;
+        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
+        StringBuilder componentConfig = new StringBuilder();
+        for (int i = 0; i < Math.min(4, components.length); i++) {
+            int j = components[i];
+            if (j > 0 && j < componentStrings.length) {
+                componentConfig.append(componentStrings[j]);
+            }
+        }
+        return componentConfig.toString();
     }
 
@@ -1162,11 +694,4 @@
 
     @Nullable
-    public String getExposureTimeDescription()
-    {
-        String value = _directory.getString(TAG_EXPOSURE_TIME);
-        return value == null ? null : value + " sec";
-    }
-
-    @Nullable
     public String getShutterSpeedDescription()
     {
@@ -1175,10 +700,355 @@
 
     @Nullable
-    public String getFNumberDescription()
-    {
-        Rational value = _directory.getRational(TAG_FNUMBER);
-        if (value == null)
-            return null;
-        return getFStopDescription(value.doubleValue());
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return getFStopDescription(fStop);
+    }
+
+    @Nullable
+    public String getBrightnessValueDescription()
+    {
+        Rational value = _directory.getRational(TAG_BRIGHTNESS_VALUE);
+        if (value == null)
+            return null;
+        if (value.getNumerator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getExposureBiasDescription()
+    {
+        Rational value = _directory.getRational(TAG_EXPOSURE_BIAS);
+        if (value == null)
+            return null;
+        return value.toSimpleString(true) + " EV";
+    }
+
+    @Nullable
+    public String getMaxApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return getFStopDescription(fStop);
+    }
+
+    @Nullable
+    public String getSubjectDistanceDescription()
+    {
+        Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE);
+        if (value == null)
+            return null;
+        if (value.getNumerator() == 0xFFFFFFFFL)
+            return "Infinity";
+        if (value.getNumerator() == 0)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " metres";
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
+        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
+        Integer value = _directory.getInteger(TAG_METERING_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Average";
+            case 2: return "Center weighted average";
+            case 3: return "Spot";
+            case 4: return "Multi-spot";
+            case 5: return "Multi-segment";
+            case 6: return "Partial";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        // See http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
+        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Daylight";
+            case 2: return "Florescent";
+            case 3: return "Tungsten";
+            case 4: return "Flash";
+            case 9: return "Fine Weather";
+            case 10: return "Cloudy";
+            case 11: return "Shade";
+            case 12: return "Daylight Fluorescent";
+            case 13: return "Day White Fluorescent";
+            case 14: return "Cool White Fluorescent";
+            case 15: return "White Fluorescent";
+            case 16: return "Warm White Fluorescent";
+            case 17: return "Standard light";
+            case 18: return "Standard light (B)";
+            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 "Studio Tungsten";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashDescription()
+    {
+        /*
+         * This is a bit mask.
+         * 0 = flash fired
+         * 1 = return detected
+         * 2 = return able to be detected
+         * 3 = unknown
+         * 4 = auto used
+         * 5 = unknown
+         * 6 = red eye reduction used
+         */
+
+        final Integer value = _directory.getInteger(TAG_FLASH);
+
+        if (value == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        if ((value & 0x1) != 0)
+            sb.append("Flash fired");
+        else
+            sb.append("Flash did not fire");
+
+        // check if we're able to detect a return, before we mention it
+        if ((value & 0x4) != 0) {
+            if ((value & 0x2) != 0)
+                sb.append(", return detected");
+            else
+                sb.append(", return not detected");
+        }
+
+        // If 0x10 is set and the lowest byte is not zero - then flash is Auto
+        if ((value & 0x10) != 0 && (value & 0x0F) != 0)
+            sb.append(", auto");
+
+        if ((value & 0x40) != 0)
+            sb.append(", red-eye reduction");
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Rational value = _directory.getRational(TAG_FOCAL_LENGTH);
+        return value == null ? null : getFocalLengthDescription(value.doubleValue());
+    }
+
+    @Nullable
+    public String getUserCommentDescription()
+    {
+        return getEncodedTextDescription(TAG_USER_COMMENT);
+    }
+
+    @Nullable
+    public String getTemperatureDescription()
+    {
+        Rational value = _directory.getRational(TAG_TEMPERATURE);
+        if (value == null)
+            return null;
+        if (value.getDenominator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0");
+        return formatter.format(value.doubleValue()) + " °C";
+    }
+
+    @Nullable
+    public String getHumidityDescription()
+    {
+        Rational value = _directory.getRational(TAG_HUMIDITY);
+        if (value == null)
+            return null;
+        if (value.getDenominator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0");
+        return formatter.format(value.doubleValue()) + " %";
+    }
+
+    @Nullable
+    public String getPressureDescription()
+    {
+        Rational value = _directory.getRational(TAG_PRESSURE);
+        if (value == null)
+            return null;
+        if (value.getDenominator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0");
+        return formatter.format(value.doubleValue()) + " hPa";
+    }
+
+    @Nullable
+    public String getWaterDepthDescription()
+    {
+        Rational value = _directory.getRational(TAG_WATER_DEPTH);
+        if (value == null)
+            return null;
+        if (value.getDenominator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " metres";
+    }
+
+    @Nullable
+    public String getAccelerationDescription()
+    {
+        Rational value = _directory.getRational(TAG_ACCELERATION);
+        if (value == null)
+            return null;
+        if (value.getDenominator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " mGal";
+    }
+
+    @Nullable
+    public String getCameraElevationAngleDescription()
+    {
+        Rational value = _directory.getRational(TAG_CAMERA_ELEVATION_ANGLE);
+        if (value == null)
+            return null;
+        if (value.getDenominator() == 0xFFFFFFFFL)
+            return "Unknown";
+        DecimalFormat formatter = new DecimalFormat("0.##");
+        return formatter.format(value.doubleValue()) + " degrees";
+    }
+
+    /** The Windows specific tags uses plain Unicode. */
+    @Nullable
+    private String getUnicodeDescription(int tag)
+    {
+        byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null)
+            return null;
+        try {
+            // Decode the unicode string and trim the unicode zero "\0" from the end.
+            return new String(bytes, "UTF-16LE").trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getWindowsTitleDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_TITLE);
+    }
+
+    @Nullable
+    public String getWindowsCommentDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_COMMENT);
+    }
+
+    @Nullable
+    public String getWindowsAuthorDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_AUTHOR);
+    }
+
+    @Nullable
+    public String getWindowsKeywordsDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_KEYWORDS);
+    }
+
+    @Nullable
+    public String getWindowsSubjectDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_SUBJECT);
+    }
+
+    @Nullable
+    public String getFlashPixVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2);
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_COLOR_SPACE);
+        if (value == null)
+            return null;
+        if (value == 1)
+            return "sRGB";
+        if (value == 65535)
+            return "Undefined";
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getExifImageWidthDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getExifImageHeightDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getFocalPlaneXResolutionDescription()
+    {
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION);
+        if (rational == null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit == null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneYResolutionDescription()
+    {
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION);
+        if (rational == null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit == null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneResolutionUnitDescription()
+    {
+        // Unit of FocalPlaneXResolution/FocalPlaneYResolution.
+        // '1' means no-unit, '2' inch, '3' centimeter.
+        return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+            1,
+            "(No unit)",
+            "Inches",
+            "cm"
+        );
     }
 
@@ -1203,32 +1073,226 @@
 
     @Nullable
-    public String getComponentConfigurationDescription()
-    {
-        int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION);
-        if (components == null)
-            return null;
-        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
-        StringBuilder componentConfig = new StringBuilder();
-        for (int i = 0; i < Math.min(4, components.length); i++) {
-            int j = components[i];
-            if (j > 0 && j < componentStrings.length) {
-                componentConfig.append(componentStrings[j]);
+    public String getFileSourceDescription()
+    {
+        return getIndexedDescription(TAG_FILE_SOURCE,
+            1,
+            "Film Scanner",
+            "Reflection Print Scanner",
+            "Digital Still Camera (DSC)"
+        );
+    }
+
+    @Nullable
+    public String getSceneTypeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_TYPE,
+            1,
+            "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>
+    /// 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;
             }
-        }
-        return componentConfig.toString();
-    }
-
-    @Nullable
-    public String getJpegProcDescription()
-    {
-        Integer value = _directory.getInteger(TAG_JPEG_PROC);
-        if (value == null)
-            return null;
-        switch (value) {
-            case 1: return "Baseline";
-            case 14: return "Lossless";
-            default:
-                return "Unknown (" + value + ")";
-        }
+            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;
+    }
+
+    @Nullable
+    public String getCustomRenderedDescription()
+    {
+        return getIndexedDescription(TAG_CUSTOM_RENDERED,
+            "Normal process",
+            "Custom process"
+        );
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(TAG_EXPOSURE_MODE,
+            "Auto exposure",
+            "Manual exposure",
+            "Auto bracket"
+        );
+    }
+
+    @Nullable
+    public String getWhiteBalanceModeDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE_MODE,
+            "Auto white balance",
+            "Manual white balance"
+        );
+    }
+
+    @Nullable
+    public String getDigitalZoomRatioDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+                ? "Digital zoom not used"
+                : new DecimalFormat("0.#").format(value.doubleValue());
+    }
+
+    @Nullable
+    public String get35mmFilmEquivFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
+        return value == null
+            ? null
+            : value == 0
+                ? "Unknown"
+                : getFocalLengthDescription(value);
+    }
+
+    @Nullable
+    public String getSceneCaptureTypeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE,
+            "Standard",
+            "Landscape",
+            "Portrait",
+            "Night scene"
+        );
+    }
+
+    @Nullable
+    public String getGainControlDescription()
+    {
+        return getIndexedDescription(TAG_GAIN_CONTROL,
+            "None",
+            "Low gain up",
+            "Low gain down",
+            "High gain up",
+            "High gain down"
+        );
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST,
+            "None",
+            "Soft",
+            "Hard"
+        );
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION,
+            "None",
+            "Low saturation",
+            "High saturation"
+        );
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS,
+            "None",
+            "Low",
+            "Hard"
+        );
+    }
+
+    @Nullable
+    public String getSubjectDistanceRangeDescription()
+    {
+        return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE,
+            "Unknown",
+            "Macro",
+            "Close view",
+            "Distant view"
+        );
+    }
+
+    @Nullable
+    public String getLensSpecificationDescription()
+    {
+        return getLensSpecificationDescription(TAG_LENS_SPECIFICATION);
     }
 }
Index: trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -189,4 +189,5 @@
     public static final int TAG_FNUMBER                           = 0x829D;
     public static final int TAG_IPTC_NAA                          = 0x83BB;
+    public static final int TAG_PHOTOSHOP_SETTINGS                = 0x8649;
     public static final int TAG_INTER_COLOR_PROFILE               = 0x8773;
     /**
@@ -230,11 +231,14 @@
     public static final int TAG_STANDARD_OUTPUT_SENSITIVITY       = 0x8831;
     public static final int TAG_RECOMMENDED_EXPOSURE_INDEX        = 0x8832;
-    /** Non-standard, but in use. */
-    public static final int TAG_TIME_ZONE_OFFSET                  = 0x882A;
-    public static final int TAG_SELF_TIMER_MODE                   = 0x882B;
+    public static final int TAG_ISO_SPEED                         = 0x8833;
+    public static final int TAG_ISO_SPEED_LATITUDE_YYY            = 0x8834;
+    public static final int TAG_ISO_SPEED_LATITUDE_ZZZ            = 0x8835;
 
     public static final int TAG_EXIF_VERSION                      = 0x9000;
     public static final int TAG_DATETIME_ORIGINAL                 = 0x9003;
     public static final int TAG_DATETIME_DIGITIZED                = 0x9004;
+    public static final int TAG_OFFSET_TIME                       = 0x9010;
+    public static final int TAG_OFFSET_TIME_ORIGINAL              = 0x9011;
+    public static final int TAG_OFFSET_TIME_DIGITIZED             = 0x9012;
 
     public static final int TAG_COMPONENTS_CONFIGURATION          = 0x9101;
@@ -357,4 +361,11 @@
     public static final int TAG_SUBSECOND_TIME_ORIGINAL           = 0x9291;
     public static final int TAG_SUBSECOND_TIME_DIGITIZED          = 0x9292;
+
+    public static final int TAG_TEMPERATURE                       = 0x9400;
+    public static final int TAG_HUMIDITY                          = 0x9401;
+    public static final int TAG_PRESSURE                          = 0x9402;
+    public static final int TAG_WATER_DEPTH                       = 0x9403;
+    public static final int TAG_ACCELERATION                      = 0x9404;
+    public static final int TAG_CAMERA_ELEVATION_ANGLE            = 0x9405;
 
     /** The image title, as used by Windows XP. */
@@ -671,4 +682,5 @@
         map.put(TAG_FNUMBER, "F-Number");
         map.put(TAG_IPTC_NAA, "IPTC/NAA");
+        map.put(TAG_PHOTOSHOP_SETTINGS, "Photoshop Settings");
         map.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile");
         map.put(TAG_EXPOSURE_PROGRAM, "Exposure Program");
@@ -682,9 +694,13 @@
         map.put(TAG_STANDARD_OUTPUT_SENSITIVITY, "Standard Output Sensitivity");
         map.put(TAG_RECOMMENDED_EXPOSURE_INDEX, "Recommended Exposure Index");
-        map.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
-        map.put(TAG_SELF_TIMER_MODE, "Self Timer Mode");
+        map.put(TAG_ISO_SPEED, "ISO Speed");
+        map.put(TAG_ISO_SPEED_LATITUDE_YYY, "ISO Speed Latitude yyy");
+        map.put(TAG_ISO_SPEED_LATITUDE_ZZZ, "ISO Speed Latitude zzz");
         map.put(TAG_EXIF_VERSION, "Exif Version");
         map.put(TAG_DATETIME_ORIGINAL, "Date/Time Original");
         map.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized");
+        map.put(TAG_OFFSET_TIME, "Offset Time");
+        map.put(TAG_OFFSET_TIME_ORIGINAL, "Offset Time Original");
+        map.put(TAG_OFFSET_TIME_DIGITIZED, "Offset Time Digitized");
         map.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration");
         map.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel");
@@ -715,4 +731,10 @@
         map.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original");
         map.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized");
+        map.put(TAG_TEMPERATURE, "Temperature");
+        map.put(TAG_HUMIDITY, "Humidity");
+        map.put(TAG_PRESSURE, "Pressure");
+        map.put(TAG_WATER_DEPTH, "Water Depth");
+        map.put(TAG_ACCELERATION, "Acceleration");
+        map.put(TAG_CAMERA_ELEVATION_ANGLE, "Camera Elevation Angle");
         map.put(TAG_WIN_TITLE, "Windows XP Title");
         map.put(TAG_WIN_COMMENT, "Windows XP Comment");
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,7 @@
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
+
 import java.util.HashMap;
-
-import com.drew.lang.annotations.NotNull;
 
 /**
Index: trunk/src/com/drew/metadata/exif/ExifImageDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifImageDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifImageDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifImageDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifImageDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifImageDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifReader.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,4 +23,5 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
 
 import java.util.Date;
@@ -67,7 +68,44 @@
 
     /**
-     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
-     * representing the date and time when this image was captured.  Attempts will be made to parse the
-     * values as though it is in the GMT {@link TimeZone}.
+     * Parses the date/time tag, the subsecond tag and the time offset tag to obtain a single Date
+     * object with milliseconds representing the date and time when this image was modified.  If
+     * the time offset tag does not exist, attempts will be made to parse the values as though it is
+     * in the GMT {@link TimeZone}.
+     *
+     * @return A Date object representing when this image was modified, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateModified()
+    {
+        return getDateModified(null);
+    }
+
+    /**
+     * Parses the date/time tag, the subsecond tag and the time offset tag to obtain a single Date
+     * object with milliseconds representing the date and time when this image was modified.  If
+     * the time offset tag does not exist, attempts will be made to parse the values as though it is
+     * in the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).
+     *
+     * @param timeZone the time zone to use
+     * @return A Date object representing when this image was modified, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateModified(@Nullable TimeZone timeZone)
+    {
+        Directory parent = getParent();
+        if (parent instanceof ExifIFD0Directory) {
+            TimeZone timeZoneModified = getTimeZone(TAG_OFFSET_TIME);
+            return parent.getDate(TAG_DATETIME, getString(TAG_SUBSECOND_TIME),
+                (timeZoneModified != null) ? timeZoneModified : timeZone);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Parses the date/time tag, the subsecond tag and the time offset tag to obtain a single Date
+     * object with milliseconds representing the date and time when this image was captured.  If
+     * the time offset tag does not exist, attempts will be made to parse the values as though it is
+     * in the GMT {@link TimeZone}.
      *
      * @return A Date object representing when this image was captured, if possible, otherwise null
@@ -80,8 +118,8 @@
 
     /**
-     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
-     * representing the date and time when this image was captured.  Attempts will be made to parse the
-     * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter
-     * (if it is non-null).
+     * Parses the date/time tag, the subsecond tag and the time offset tag to obtain a single Date
+     * object with milliseconds representing the date and time when this image was captured.  If
+     * the time offset tag does not exist, attempts will be made to parse the values as though it is
+     * in the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).
      *
      * @param timeZone the time zone to use
@@ -91,11 +129,14 @@
     public Date getDateOriginal(@Nullable TimeZone timeZone)
     {
-        return getDate(TAG_DATETIME_ORIGINAL, getString(TAG_SUBSECOND_TIME_ORIGINAL), timeZone);
+        TimeZone timeZoneOriginal = getTimeZone(TAG_OFFSET_TIME_ORIGINAL);
+        return getDate(TAG_DATETIME_ORIGINAL, getString(TAG_SUBSECOND_TIME_ORIGINAL),
+            (timeZoneOriginal != null) ? timeZoneOriginal : timeZone);
     }
 
     /**
-     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
-     * representing the date and time when this image was digitized.  Attempts will be made to parse the
-     * values as though it is in the GMT {@link TimeZone}.
+     * Parses the date/time tag, the subsecond tag and the time offset tag to obtain a single Date
+     * object with milliseconds representing the date and time when this image was digitized.  If
+     * the time offset tag does not exist, attempts will be made to parse the values as though it is
+     * in the GMT {@link TimeZone}.
      *
      * @return A Date object representing when this image was digitized, if possible, otherwise null
@@ -108,8 +149,8 @@
 
     /**
-     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
-     * representing the date and time when this image was digitized.  Attempts will be made to parse the
-     * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter
-     * (if it is non-null).
+     * Parses the date/time tag, the subsecond tag and the time offset tag to obtain a single Date
+     * object with milliseconds representing the date and time when this image was digitized.  If
+     * the time offset tag does not exist, attempts will be made to parse the values as though it is
+     * in the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).
      *
      * @param timeZone the time zone to use
@@ -119,5 +160,18 @@
     public Date getDateDigitized(@Nullable TimeZone timeZone)
     {
-        return getDate(TAG_DATETIME_DIGITIZED, getString(TAG_SUBSECOND_TIME_DIGITIZED), timeZone);
+        TimeZone timeZoneDigitized = getTimeZone(TAG_OFFSET_TIME_DIGITIZED);
+        return getDate(TAG_DATETIME_DIGITIZED, getString(TAG_SUBSECOND_TIME_DIGITIZED),
+            (timeZoneDigitized != null) ? timeZoneDigitized : timeZone);
+    }
+
+    @Nullable
+    private TimeZone getTimeZone(int tagType)
+    {
+        String timeOffset = getString(tagType);
+        if (timeOffset != null && timeOffset.matches("[\\+\\-]\\d\\d:\\d\\d")) {
+            return TimeZone.getTimeZone("GMT" + timeOffset);
+        } else {
+            return null;
+        }
     }
 }
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,9 +22,8 @@
 package com.drew.metadata.exif;
 
-import static com.drew.metadata.exif.ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH;
-import static com.drew.metadata.exif.ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET;
-
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
+
+import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
 
 /**
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/ExifTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,4 +30,5 @@
 import com.drew.imaging.tiff.TiffProcessingException;
 import com.drew.imaging.tiff.TiffReader;
+import com.drew.lang.BufferBoundsException;
 import com.drew.lang.Charsets;
 import com.drew.lang.RandomAccessReader;
@@ -38,5 +39,33 @@
 import com.drew.metadata.Metadata;
 import com.drew.metadata.StringValue;
-import com.drew.metadata.exif.makernotes.*;
+import com.drew.metadata.exif.makernotes.AppleMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.CanonMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.CasioType1MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.CasioType2MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.FujifilmMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.KodakMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.KyoceraMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.LeicaMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.LeicaType5MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.NikonType1MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.NikonType2MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusEquipmentMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusFocusInfoMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusRawDevelopment2MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusRawDevelopmentMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.OlympusRawInfoMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.PentaxMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.ReconyxHyperFireMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.ReconyxUltraFireMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.RicohMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.SamsungType2MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.SanyoMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.SigmaMakernoteDirectory;
+import com.drew.metadata.exif.makernotes.SonyType1MakernoteDirectory;
+import com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory;
 import com.drew.metadata.iptc.IptcReader;
 import com.drew.metadata.tiff.DirectoryTiffHandler;
@@ -54,8 +83,5 @@
     public ExifTiffHandler(@NotNull Metadata metadata, @Nullable Directory parentDirectory)
     {
-        super(metadata);
-
-        if (parentDirectory != null)
-            _currentDirectory.setParent(parentDirectory);
+        super(metadata, parentDirectory);
     }
 
@@ -67,9 +93,8 @@
         final int panasonicRawTiffMarker = 0x0055; // for RW2 files
 
-        switch (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
+            case olympusRawTiffMarker:      // TODO implement an IFD0, if there is one
+            case olympusRawTiffMarker2:     // TODO implement an IFD0, if there is one
                 pushDirectory(ExifIFD0Directory.class);
                 break;
@@ -172,5 +197,5 @@
 
         // an unknown (0) formatCode needs to be potentially handled later as a highly custom directory tag
-        if(formatCode == 0)
+        if (formatCode == 0)
             return 0L;
 
@@ -185,9 +210,9 @@
                                     final int byteCount) throws IOException
     {
+        assert(_currentDirectory != null);
+
         // Some 0x0000 tags have a 0 byteCount. Determine whether it's bad.
-        if (tagId == 0)
-        {
-            if (_currentDirectory.containsTag(tagId))
-            {
+        if (tagId == 0) {
+            if (_currentDirectory.containsTag(tagId)) {
                 // Let it go through for now. Some directories handle it, some don't
                 return false;
@@ -215,10 +240,10 @@
         }
 
-        if (HandlePrintIM(_currentDirectory, tagId))
+        if (handlePrintIM(_currentDirectory, tagId))
         {
             PrintIMDirectory printIMDirectory = new PrintIMDirectory();
             printIMDirectory.setParent(_currentDirectory);
             _metadata.addDirectory(printIMDirectory);
-            ProcessPrintIM(printIMDirectory, tagOffset, reader, byteCount);
+            processPrintIM(printIMDirectory, tagOffset, reader, byteCount);
             return true;
         }
@@ -226,8 +251,6 @@
         // 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)
-            {
+        if (_currentDirectory instanceof OlympusMakernoteDirectory) {
+            switch (tagId) {
                 case OlympusMakernoteDirectory.TAG_EQUIPMENT:
                     pushDirectory(OlympusEquipmentMakernoteDirectory.class);
@@ -265,15 +288,13 @@
         }
 
-        if (_currentDirectory instanceof PanasonicRawIFD0Directory)
-        {
+        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)
-            {
+            switch (tagId) {
                 case PanasonicRawIFD0Directory.TagWbInfo:
                     PanasonicRawWbInfoDirectory dirWbInfo = new PanasonicRawWbInfoDirectory();
                     dirWbInfo.setParent(_currentDirectory);
                     _metadata.addDirectory(dirWbInfo);
-                    ProcessBinary(dirWbInfo, tagOffset, reader, byteCount, false, 2);
+                    processBinary(dirWbInfo, tagOffset, reader, byteCount, false, 2);
                     return true;
                 case PanasonicRawIFD0Directory.TagWbInfo2:
@@ -281,5 +302,5 @@
                     dirWbInfo2.setParent(_currentDirectory);
                     _metadata.addDirectory(dirWbInfo2);
-                    ProcessBinary(dirWbInfo2, tagOffset, reader, byteCount, false, 3);
+                    processBinary(dirWbInfo2, tagOffset, reader, byteCount, false, 3);
                     return true;
                 case PanasonicRawIFD0Directory.TagDistortionInfo:
@@ -287,5 +308,5 @@
                     dirDistort.setParent(_currentDirectory);
                     _metadata.addDirectory(dirDistort);
-                    ProcessBinary(dirDistort, tagOffset, reader, byteCount, true, 1);
+                    processBinary(dirDistort, tagOffset, reader, byteCount, true, 1);
                     return true;
             }
@@ -293,6 +314,5 @@
 
         // Panasonic RAW sometimes contains an embedded version of the data as a JPG file.
-        if (tagId == PanasonicRawIFD0Directory.TagJpgFromRaw && _currentDirectory instanceof PanasonicRawIFD0Directory)
-        {
+        if (tagId == PanasonicRawIFD0Directory.TagJpgFromRaw && _currentDirectory instanceof PanasonicRawIFD0Directory) {
             byte[] jpegrawbytes = reader.getBytes(tagOffset, byteCount);
 
@@ -316,35 +336,27 @@
     }
 
-    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
+    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 = 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))
-            {
+        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)
+                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
-                {
+                } else {
                     // the next arrayLength bytes are a multi-byte value
-                    if (issigned)
-                    {
+                    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
-                    {
+                    } else {
                         int[] val = new int[arrayLength];
                         for (int j = 0; j<val.length; j++)
@@ -359,4 +371,24 @@
     }
 
+    /** Read a given number of bytes from the stream
+     *
+     * This method is employed to "suppress" attempts to read beyond end of the
+     * file as may happen at the beginning of processMakernote when we read
+     * increasingly longer camera makes.
+     *
+     * Instead of failing altogether in this context we return an empty string
+     * which will fail all sensible attempts to compare to makes while avoiding
+     * a full-on failure.
+     */
+    @NotNull
+    private static String getReaderString(final @NotNull RandomAccessReader reader, final int makernoteOffset, final int bytesRequested) throws IOException
+    {
+        try {
+            return reader.getString(makernoteOffset, bytesRequested, Charsets.UTF_8);
+        } catch(BufferBoundsException e) {
+            return "";
+        }
+    }
+
     private boolean processMakernote(final int makernoteOffset,
                                      final @NotNull Set<Integer> processedIfdOffsets,
@@ -364,4 +396,6 @@
                                      final @NotNull RandomAccessReader reader) throws IOException
     {
+        assert(_currentDirectory != null);
+
         // Determine the camera model and makernote format.
         Directory ifd0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
@@ -369,14 +403,14 @@
         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);
+        final String firstTwoChars    = getReaderString(reader, makernoteOffset, 2);
+        final String firstThreeChars  = getReaderString(reader, makernoteOffset, 3);
+        final String firstFourChars   = getReaderString(reader, makernoteOffset, 4);
+        final String firstFiveChars   = getReaderString(reader, makernoteOffset, 5);
+        final String firstSixChars    = getReaderString(reader, makernoteOffset, 6);
+        final String firstSevenChars  = getReaderString(reader, makernoteOffset, 7);
+        final String firstEightChars  = getReaderString(reader, makernoteOffset, 8);
+        final String firstNineChars   = getReaderString(reader, makernoteOffset, 9);
+        final String firstTenChars    = getReaderString(reader, makernoteOffset, 10);
+        final String firstTwelveChars = getReaderString(reader, makernoteOffset, 12);
 
         boolean byteOrderBefore = reader.isMotorolaByteOrder();
@@ -501,5 +535,5 @@
                 return false;
             }
-        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12, Charsets.UTF_8))) {
+        } else if ("Panasonic\u0000\u0000\u0000".equals(firstTwelveChars)) {
             // 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
@@ -575,11 +609,10 @@
     }
 
-    private static Boolean HandlePrintIM(@NotNull final Directory directory, final int tagId)
+    private static boolean handlePrintIM(@NotNull final Directory directory, final int tagId)
     {
         if (tagId == ExifDirectoryBase.TAG_PRINT_IMAGE_MATCHING_INFO)
             return true;
 
-        if (tagId == 0x0E00)
-        {
+        if (tagId == 0x0E00) {
             // Tempting to say every tagid of 0x0E00 is a PIM tag, but can't be 100% sure
             if (directory instanceof CasioType2MakernoteDirectory ||
@@ -606,16 +639,14 @@
     /// 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
+    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)
-        {
+        if (byteCount == 0) {
             directory.addError("Empty PrintIM data");
             return;
         }
 
-        if (byteCount <= 15)
-        {
+        if (byteCount <= 15) {
             directory.addError("Bad PrintIM data");
             return;
@@ -624,6 +655,5 @@
         String header = reader.getString(tagValueOffset, 12, Charsets.UTF_8);
 
-        if (!header.startsWith("PrintIM")) //, StringComparison.Ordinal))
-        {
+        if (!header.startsWith("PrintIM")) {
             directory.addError("Invalid PrintIM header");
             return;
@@ -632,12 +662,11 @@
         // check size of PrintIM block
         int num = reader.getUInt16(tagValueOffset + 14);
-        if (byteCount < 16 + num * 6)
-        {
+
+        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)
-            {
+            if (byteCount < 16 + num * 6) {
                 directory.addError("Bad PrintIM size");
                 return;
@@ -647,6 +676,5 @@
         directory.setObject(PrintIMDirectory.TagPrintImVersion, header.substring(8, 12));
 
-        for (int n = 0; n < num; n++)
-        {
+        for (int n = 0; n < num; n++) {
             int pos = tagValueOffset + 16 + n * 6;
             int tag = reader.getUInt16(pos);
@@ -712,10 +740,8 @@
             build = null;
         }
-        if (build != null)
-        {
+
+        if (build != null) {
             directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d.%s", major, minor, revision, build));
-        }
-        else
-        {
+        } 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.");
@@ -746,11 +772,8 @@
             (month >= 1 && month < 13) &&
             (day >= 1 && day < 32) &&
-            (year >= 1 && year <= 9999))
-        {
+            (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
-        {
+        } else {
             directory.addError("Error processing Reconyx HyperFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time.");
         }
@@ -778,6 +801,5 @@
         /*uint makernoteID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteID));
         directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteID, makernoteID);
-        if (makernoteID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_ID)
-        {
+        if (makernoteID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_ID) {
             directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote ID 0x" + makernoteID.ToString("x8"));
             return;
@@ -786,6 +808,5 @@
         uint makernotePublicID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID));
         directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID, makernotePublicID);
-        if (makernotePublicID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_PUBLIC_ID)
-        {
+        if (makernotePublicID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_PUBLIC_ID) {
             directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote Public ID 0x" + makernotePublicID.ToString("x8"));
             return;
@@ -818,10 +839,7 @@
             (month >= 1 && month < 13) &&
             (day >= 1 && day < 32) &&
-            (year >= 1 && year <= 9999))
-        {
+            (year >= 1 && year <= 9999)) {
             directory.Set(ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL, new DateTime(year, month, day, hour, minutes, seconds, DateTimeKind.Unspecified));
-        }
-        else
-        {
+        } else {
             directory.addError("Error processing Reconyx UltraFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time.");
         }*/
Index: trunk/src/com/drew/metadata/exif/GpsDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -59,6 +59,10 @@
             case TAG_MEASURE_MODE:
                 return getGpsMeasureModeDescription();
+            case TAG_DOP:
+                return getGpsDopDescription();
             case TAG_SPEED_REF:
                 return getGpsSpeedRefDescription();
+            case TAG_SPEED:
+                return getGpsSpeedDescription();
             case TAG_TRACK_REF:
             case TAG_IMG_DIRECTION_REF:
@@ -69,6 +73,12 @@
             case TAG_DEST_BEARING:
                 return getGpsDirectionDescription(tagType);
+            case TAG_DEST_LATITUDE:
+                return getGpsDestLatitudeDescription();
+            case TAG_DEST_LONGITUDE:
+                return getGpsDestLongitudeDescription();
             case TAG_DEST_DISTANCE_REF:
                 return getGpsDestinationReferenceDescription();
+            case TAG_DEST_DISTANCE:
+                return getGpsDestDistanceDescription();
             case TAG_TIME_STAMP:
                 return getGpsTimeStampDescription();
@@ -79,6 +89,12 @@
                 // three rational numbers -- displayed in HH"MM"SS.ss
                 return getGpsLatitudeDescription();
+            case TAG_PROCESSING_METHOD:
+                return getGpsProcessingMethodDescription();
+            case TAG_AREA_INFORMATION:
+                return getGpsAreaInformationDescription();
             case TAG_DIFFERENTIAL:
                 return getGpsDifferentialDescription();
+            case TAG_H_POSITIONING_ERROR:
+                return getGpsHPositioningErrorDescription();
             default:
                 return super.getDescription(tagType);
@@ -121,4 +137,34 @@
 
     @Nullable
+    public String getGpsDestLatitudeDescription()
+    {
+        Rational[] latitudes = _directory.getRationalArray(TAG_DEST_LATITUDE);
+        String latitudeRef = _directory.getString(TAG_DEST_LATITUDE_REF);
+
+        if (latitudes == null || latitudes.length != 3 || latitudeRef == null)
+            return null;
+
+        Double lat = GeoLocation.degreesMinutesSecondsToDecimal(
+            latitudes[0], latitudes[1], latitudes[2], latitudeRef.equalsIgnoreCase("S"));
+
+        return lat == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(lat);
+    }
+
+    @Nullable
+    public String getGpsDestLongitudeDescription()
+    {
+        Rational[] longitudes = _directory.getRationalArray(TAG_LONGITUDE);
+        String longitudeRef = _directory.getString(TAG_LONGITUDE_REF);
+
+        if (longitudes == null || longitudes.length != 3 || longitudeRef == null)
+            return null;
+
+        Double lon = GeoLocation.degreesMinutesSecondsToDecimal(
+            longitudes[0], longitudes[1], longitudes[2], longitudeRef.equalsIgnoreCase("W"));
+
+        return lon == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(lon);
+    }
+
+    @Nullable
     public String getGpsDestinationReferenceDescription()
     {
@@ -139,4 +185,16 @@
 
     @Nullable
+    public String getGpsDestDistanceDescription()
+    {
+        final Rational value = _directory.getRational(TAG_DEST_DISTANCE);
+        if (value == null)
+            return null;
+        final String unit = getGpsDestinationReferenceDescription();
+        return String.format("%s %s",
+            new DecimalFormat("0.##").format(value.doubleValue()),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
     public String getGpsDirectionDescription(int tagType)
     {
@@ -166,4 +224,11 @@
 
     @Nullable
+    public String getGpsDopDescription()
+    {
+        final Rational value = _directory.getRational(TAG_DOP);
+        return value == null ? null : new DecimalFormat("0.##").format(value.doubleValue());
+    }
+
+    @Nullable
     public String getGpsSpeedRefDescription()
     {
@@ -173,5 +238,5 @@
         String gpsSpeedRef = value.trim();
         if ("K".equalsIgnoreCase(gpsSpeedRef)) {
-            return "kph";
+            return "km/h";
         } else if ("M".equalsIgnoreCase(gpsSpeedRef)) {
             return "mph";
@@ -181,4 +246,16 @@
             return "Unknown (" + gpsSpeedRef + ")";
         }
+    }
+
+    @Nullable
+    public String getGpsSpeedDescription()
+    {
+        final Rational value = _directory.getRational(TAG_SPEED);
+        if (value == null)
+            return null;
+        final String unit = getGpsSpeedRefDescription();
+        return String.format("%s %s",
+            new DecimalFormat("0.##").format(value.doubleValue()),
+            unit == null ? "unit" : unit.toLowerCase());
     }
 
@@ -225,5 +302,17 @@
     {
         final Rational value = _directory.getRational(TAG_ALTITUDE);
-        return value == null ? null : value.intValue() + " metres";
+        return value == null ? null : new DecimalFormat("0.##").format(value.doubleValue()) + " metres";
+    }
+
+    @Nullable
+    public String getGpsProcessingMethodDescription()
+    {
+        return getEncodedTextDescription(TAG_PROCESSING_METHOD);
+    }
+
+    @Nullable
+    public String getGpsAreaInformationDescription()
+    {
+        return getEncodedTextDescription(TAG_AREA_INFORMATION);
     }
 
@@ -232,4 +321,11 @@
     {
         return getIndexedDescription(TAG_DIFFERENTIAL, "No Correction", "Differential Corrected");
+    }
+
+    @Nullable
+    public String getGpsHPositioningErrorDescription()
+    {
+        final Rational value = _directory.getRational(TAG_H_POSITIONING_ERROR);
+        return value == null ? null : new DecimalFormat("0.##").format(value.doubleValue()) + " metres";
     }
 
Index: trunk/src/com/drew/metadata/exif/GpsDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -95,10 +95,14 @@
     /** Distance to destination GPSDestDistance 26 1A RATIONAL 1 */
     public static final int TAG_DEST_DISTANCE = 0x001A;
-
-    /** Values of "GPS", "CELLID", "WLAN" or "MANUAL" by the EXIF spec. */
+    /** Name of the method used for location finding GPSProcessingMethod 27 1B UNDEFINED Any */
     public static final int TAG_PROCESSING_METHOD = 0x001B;
+    /** Name of the GPS area GPSAreaInformation 28 1C UNDEFINED Any */
     public static final int TAG_AREA_INFORMATION = 0x001C;
+    /** Date and time GPSDateStamp 29 1D ASCII 11 */
     public static final int TAG_DATE_STAMP = 0x001D;
+    /** Whether differential correction is applied GPSDifferential 30 1E SHORT 1 */
     public static final int TAG_DIFFERENTIAL = 0x001E;
+    /** Horizontal positioning errors GPSHPositioningError 31 1F RATIONAL 1 */
+    public static final int TAG_H_POSITIONING_ERROR = 0x001F;
 
     @NotNull
@@ -140,4 +144,5 @@
         _tagNameMap.put(TAG_DATE_STAMP, "GPS Date Stamp");
         _tagNameMap.put(TAG_DIFFERENTIAL, "GPS Differential");
+        _tagNameMap.put(TAG_H_POSITIONING_ERROR, "GPS H Positioning Error");
     }
 
Index: trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Directory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Directory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawIFD0Directory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PrintIMDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PrintIMDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PrintIMDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/PrintIMDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PrintIMDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/PrintIMDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,4 +26,5 @@
 import com.drew.metadata.TagDescriptor;
 
+import java.nio.ByteBuffer;
 import java.text.DecimalFormat;
 
@@ -105,5 +106,13 @@
     public String getPowerUpTimeDescription()
     {
-        return getEpochTimeDescription(TAG_POWER_UP_TIME);
+        // this is generally a byte[] of length 8 directly representing a date and time.
+        // the format is : first 2 bytes together are the year, and then each byte after
+        //                 is month, day, hour, minute, second with the eighth byte unused
+        // e.g., 2011:04:25 01:54:58
+
+        byte[] values = _directory.getByteArray(TAG_POWER_UP_TIME);
+        short year = ByteBuffer.wrap(new byte[]{values[0], values[1]}).getShort();
+        return String.format("%04d:%02d:%02d %02d:%02d:%02d", year, values[2], values[3], 
+                                                        values[4], values[5], values[6]);
     }
 
@@ -334,4 +343,15 @@
 
     @Nullable
+    public String getLensFocusDistance()
+    {
+        int[] values = _directory.getDecryptedIntArray(TAG_LENS_DATA);
+
+        if (values == null || values.length < 11)
+            return null;
+
+        return String.format("%.2fm", getDistanceInMeters(values[10]));
+    }
+
+    @Nullable
     public String getHueAdjustmentDescription()
     {
@@ -351,3 +371,10 @@
         return getVersionBytesDescription(TAG_FIRMWARE_VERSION, 2);
     }
+
+    private double getDistanceInMeters(int val)
+    {
+        if (val < 0)
+            val += 256;
+        return 0.01 * Math.pow(10, val / 40.0f);
+    }
 }
Index: trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,4 +21,5 @@
 package com.drew.metadata.exif.makernotes;
 
+import com.drew.lang.annotations.Nullable;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
@@ -923,3 +924,67 @@
         return _tagNameMap;
     }
+
+	/** Nikon decryption tables used in exiftool */
+    private static final int[] _decTable1 =   {0xc1,0xbf,0x6d,0x0d,0x59,0xc5,0x13,0x9d,0x83,0x61,0x6b,0x4f,0xc7,0x7f,0x3d,0x3d,
+                                               0x53,0x59,0xe3,0xc7,0xe9,0x2f,0x95,0xa7,0x95,0x1f,0xdf,0x7f,0x2b,0x29,0xc7,0x0d,
+                                               0xdf,0x07,0xef,0x71,0x89,0x3d,0x13,0x3d,0x3b,0x13,0xfb,0x0d,0x89,0xc1,0x65,0x1f,
+                                               0xb3,0x0d,0x6b,0x29,0xe3,0xfb,0xef,0xa3,0x6b,0x47,0x7f,0x95,0x35,0xa7,0x47,0x4f,
+                                               0xc7,0xf1,0x59,0x95,0x35,0x11,0x29,0x61,0xf1,0x3d,0xb3,0x2b,0x0d,0x43,0x89,0xc1,
+                                               0x9d,0x9d,0x89,0x65,0xf1,0xe9,0xdf,0xbf,0x3d,0x7f,0x53,0x97,0xe5,0xe9,0x95,0x17,
+                                               0x1d,0x3d,0x8b,0xfb,0xc7,0xe3,0x67,0xa7,0x07,0xf1,0x71,0xa7,0x53,0xb5,0x29,0x89,
+                                               0xe5,0x2b,0xa7,0x17,0x29,0xe9,0x4f,0xc5,0x65,0x6d,0x6b,0xef,0x0d,0x89,0x49,0x2f,
+                                               0xb3,0x43,0x53,0x65,0x1d,0x49,0xa3,0x13,0x89,0x59,0xef,0x6b,0xef,0x65,0x1d,0x0b,
+                                               0x59,0x13,0xe3,0x4f,0x9d,0xb3,0x29,0x43,0x2b,0x07,0x1d,0x95,0x59,0x59,0x47,0xfb,
+                                               0xe5,0xe9,0x61,0x47,0x2f,0x35,0x7f,0x17,0x7f,0xef,0x7f,0x95,0x95,0x71,0xd3,0xa3,
+                                               0x0b,0x71,0xa3,0xad,0x0b,0x3b,0xb5,0xfb,0xa3,0xbf,0x4f,0x83,0x1d,0xad,0xe9,0x2f,
+                                               0x71,0x65,0xa3,0xe5,0x07,0x35,0x3d,0x0d,0xb5,0xe9,0xe5,0x47,0x3b,0x9d,0xef,0x35,
+                                               0xa3,0xbf,0xb3,0xdf,0x53,0xd3,0x97,0x53,0x49,0x71,0x07,0x35,0x61,0x71,0x2f,0x43,
+                                               0x2f,0x11,0xdf,0x17,0x97,0xfb,0x95,0x3b,0x7f,0x6b,0xd3,0x25,0xbf,0xad,0xc7,0xc5,
+                                               0xc5,0xb5,0x8b,0xef,0x2f,0xd3,0x07,0x6b,0x25,0x49,0x95,0x25,0x49,0x6d,0x71,0xc7 };
+    private static final int[] _decTable2 = { 0xa7,0xbc,0xc9,0xad,0x91,0xdf,0x85,0xe5,0xd4,0x78,0xd5,0x17,0x46,0x7c,0x29,0x4c,
+                                               0x4d,0x03,0xe9,0x25,0x68,0x11,0x86,0xb3,0xbd,0xf7,0x6f,0x61,0x22,0xa2,0x26,0x34,
+                                               0x2a,0xbe,0x1e,0x46,0x14,0x68,0x9d,0x44,0x18,0xc2,0x40,0xf4,0x7e,0x5f,0x1b,0xad,
+                                               0x0b,0x94,0xb6,0x67,0xb4,0x0b,0xe1,0xea,0x95,0x9c,0x66,0xdc,0xe7,0x5d,0x6c,0x05,
+                                               0xda,0xd5,0xdf,0x7a,0xef,0xf6,0xdb,0x1f,0x82,0x4c,0xc0,0x68,0x47,0xa1,0xbd,0xee,
+                                               0x39,0x50,0x56,0x4a,0xdd,0xdf,0xa5,0xf8,0xc6,0xda,0xca,0x90,0xca,0x01,0x42,0x9d,
+                                               0x8b,0x0c,0x73,0x43,0x75,0x05,0x94,0xde,0x24,0xb3,0x80,0x34,0xe5,0x2c,0xdc,0x9b,
+                                               0x3f,0xca,0x33,0x45,0xd0,0xdb,0x5f,0xf5,0x52,0xc3,0x21,0xda,0xe2,0x22,0x72,0x6b,
+                                               0x3e,0xd0,0x5b,0xa8,0x87,0x8c,0x06,0x5d,0x0f,0xdd,0x09,0x19,0x93,0xd0,0xb9,0xfc,
+                                               0x8b,0x0f,0x84,0x60,0x33,0x1c,0x9b,0x45,0xf1,0xf0,0xa3,0x94,0x3a,0x12,0x77,0x33,
+                                               0x4d,0x44,0x78,0x28,0x3c,0x9e,0xfd,0x65,0x57,0x16,0x94,0x6b,0xfb,0x59,0xd0,0xc8,
+                                               0x22,0x36,0xdb,0xd2,0x63,0x98,0x43,0xa1,0x04,0x87,0x86,0xf7,0xa6,0x26,0xbb,0xd6,
+                                               0x59,0x4d,0xbf,0x6a,0x2e,0xaa,0x2b,0xef,0xe6,0x78,0xb6,0x4e,0xe0,0x2f,0xdc,0x7c,
+                                               0xbe,0x57,0x19,0x32,0x7e,0x2a,0xd0,0xb8,0xba,0x29,0x00,0x3c,0x52,0x7d,0xa8,0x49,
+                                               0x3b,0x2d,0xeb,0x25,0x49,0xfa,0xa3,0xaa,0x39,0xa7,0xc5,0xa7,0x50,0x11,0x36,0xfb,
+                                               0xc6,0x67,0x4a,0xf5,0xa5,0x12,0x65,0x7e,0xb0,0xdf,0xaf,0x4e,0xb3,0x61,0x7f,0x2f };
+
+
+    /** decryption algorithm adapted from exiftool */
+    @Nullable
+    public int[] getDecryptedIntArray(int tagType)
+    {
+        int[] data = getIntArray(tagType);
+        Integer serial = getInteger(TAG_CAMERA_SERIAL_NUMBER);
+        Integer count = getInteger(TAG_EXPOSURE_SEQUENCE_NUMBER);
+
+        if (data == null || serial == null || count == null)
+            return null;
+
+        int key = 0;
+        for (int i = 0; i < 4; i++)
+            key ^= (count >> (i * 8)) & 0xff;
+
+        int ci = _decTable1[serial & 0xff];
+        int cj = _decTable2[key];
+        int ck = 0x60;
+
+        for (int i = 4; i < data.length; i++)
+        {
+            cj = (cj + ci * ck) & 0xff;
+            ck = (ck + 1) & 0xff;
+            data[i] ^= cj;
+        }
+
+        return data;
+    }
 }
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java	(revision 15217)
@@ -111,6 +111,10 @@
         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);
+        if (((v >> 3) & 1) != 0) sb.append("Noise Filter (Auto), ");
+        
+        if (sb.length() > 2) {
+            sb.delete(sb.length() - 2, sb.length());
+        }
+        return sb.toString();
     }
 
Index: trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 15216)
+++ 	(revision )
@@ -1,63 +1,0 @@
-/*
- * 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.file;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.TagDescriptor;
-
-import static com.drew.metadata.file.FileMetadataDirectory.*;
-
-/**
- * @author Drew Noakes https://drewnoakes.com
- */
-@SuppressWarnings("WeakerAccess")
-public class FileMetadataDescriptor extends TagDescriptor<FileMetadataDirectory>
-{
-    public FileMetadataDescriptor(@NotNull FileMetadataDirectory directory)
-    {
-        super(directory);
-    }
-
-    @Override
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case TAG_FILE_SIZE:
-                return getFileSizeDescription();
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    @Nullable
-    private String getFileSizeDescription()
-    {
-        Long size = _directory.getLongObject(TAG_FILE_SIZE);
-
-        if (size == null)
-            return null;
-
-        return Long.toString(size) + " bytes";
-    }
-}
-
Index: trunk/src/com/drew/metadata/file/FileMetadataDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 15216)
+++ 	(revision )
@@ -1,65 +1,0 @@
-/*
- * 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.file;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
-
-import java.util.HashMap;
-
-/**
- * @author Drew Noakes https://drewnoakes.com
- */
-@SuppressWarnings("WeakerAccess")
-public class FileMetadataDirectory extends Directory
-{
-    public static final int TAG_FILE_NAME = 1;
-    public static final int TAG_FILE_SIZE = 2;
-    public static final int TAG_FILE_MODIFIED_DATE = 3;
-
-    @NotNull
-    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-
-    static {
-        _tagNameMap.put(TAG_FILE_NAME, "File Name");
-        _tagNameMap.put(TAG_FILE_SIZE, "File Size");
-        _tagNameMap.put(TAG_FILE_MODIFIED_DATE, "File Modified Date");
-    }
-
-    public FileMetadataDirectory()
-    {
-        this.setDescriptor(new FileMetadataDescriptor(this));
-    }
-
-    @Override
-    @NotNull
-    public String getName()
-    {
-        return "File";
-    }
-
-    @Override
-    @NotNull
-    protected HashMap<Integer, String> getTagNameMap()
-    {
-        return _tagNameMap;
-    }
-}
Index: trunk/src/com/drew/metadata/file/FileMetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 15216)
+++ 	(revision )
@@ -1,49 +1,0 @@
-/*
- * 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.file;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Metadata;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Date;
-
-public class FileMetadataReader
-{
-    public void read(@NotNull File file, @NotNull Metadata metadata) throws IOException
-    {
-        if (!file.isFile())
-            throw new IOException("File object must reference a file");
-        if (!file.exists())
-            throw new IOException("File does not exist");
-        if (!file.canRead())
-            throw new IOException("File is not readable");
-
-        FileMetadataDirectory directory = new FileMetadataDirectory();
-
-        directory.setString(FileMetadataDirectory.TAG_FILE_NAME, file.getName());
-        directory.setLong(FileMetadataDirectory.TAG_FILE_SIZE, file.length());
-        directory.setDate(FileMetadataDirectory.TAG_FILE_MODIFIED_DATE, new Date(file.lastModified()));
-
-        metadata.addDirectory(directory);
-    }
-}
Index: trunk/src/com/drew/metadata/file/FileSystemDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileSystemDescriptor.java	(revision 15217)
+++ trunk/src/com/drew/metadata/file/FileSystemDescriptor.java	(revision 15217)
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.file.FileSystemDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class FileSystemDescriptor extends TagDescriptor<FileSystemDirectory>
+{
+    public FileSystemDescriptor(@NotNull FileSystemDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_FILE_SIZE:
+                return getFileSizeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getFileSizeDescription()
+    {
+        Long size = _directory.getLongObject(TAG_FILE_SIZE);
+
+        if (size == null)
+            return null;
+
+        return Long.toString(size) + " bytes";
+    }
+}
+
Index: trunk/src/com/drew/metadata/file/FileSystemDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileSystemDirectory.java	(revision 15217)
+++ trunk/src/com/drew/metadata/file/FileSystemDirectory.java	(revision 15217)
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class FileSystemDirectory extends Directory
+{
+    public static final int TAG_FILE_NAME = 1;
+    public static final int TAG_FILE_SIZE = 2;
+    public static final int TAG_FILE_MODIFIED_DATE = 3;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_FILE_NAME, "File Name");
+        _tagNameMap.put(TAG_FILE_SIZE, "File Size");
+        _tagNameMap.put(TAG_FILE_MODIFIED_DATE, "File Modified Date");
+    }
+
+    public FileSystemDirectory()
+    {
+        this.setDescriptor(new FileSystemDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "File";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/file/FileSystemMetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileSystemMetadataReader.java	(revision 15217)
+++ trunk/src/com/drew/metadata/file/FileSystemMetadataReader.java	(revision 15217)
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.*;
+import java.util.Date;
+
+public class FileSystemMetadataReader
+{
+    public void read(@NotNull File file, @NotNull Metadata metadata) throws IOException
+    {
+        if (!file.isFile())
+            throw new IOException("File object must reference a file");
+        if (!file.exists())
+            throw new IOException("File does not exist");
+        if (!file.canRead())
+            throw new IOException("File is not readable");
+
+        FileSystemDirectory directory = metadata.getFirstDirectoryOfType(FileSystemDirectory.class);
+
+        if (directory == null) {
+            directory = new FileSystemDirectory();
+            metadata.addDirectory(directory);
+        }
+
+        directory.setString(FileSystemDirectory.TAG_FILE_NAME, file.getName());
+        directory.setLong(FileSystemDirectory.TAG_FILE_SIZE, file.length());
+        directory.setDate(FileSystemDirectory.TAG_FILE_MODIFIED_DATE, new Date(file.lastModified()));
+    }
+}
Index: trunk/src/com/drew/metadata/iptc/IptcDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/iptc/IptcDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/iptc/IptcReader.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 15216)
+++ trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -118,6 +118,6 @@
             }
 
-            // we need at least five bytes left to read a tag
-            if (offset + 5 > length) {
+            // we need at least four bytes left to read a tag
+            if (offset + 4 > length) {
                 directory.addError("Too few bytes remain for a valid IPTC tag");
                 return;
@@ -130,6 +130,10 @@
                 directoryType = reader.getUInt8();
                 tagType = reader.getUInt8();
-                // TODO support Extended DataSet Tag (see 1.5(c), p14, IPTC-IIMV4.2.pdf)
                 tagByteCount = reader.getUInt16();
+                if (tagByteCount > 32767) {
+                    // Extended DataSet Tag (see 1.5(c), p14, IPTC-IIMV4.2.pdf)
+                    tagByteCount = ((tagByteCount & 0x7FFF) << 16) | reader.getUInt16();
+                    offset += 2;
+                }
                 offset += 4;
             } catch (IOException e) {
Index: trunk/src/com/drew/metadata/iptc/Iso2022Converter.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 15216)
+++ trunk/src/com/drew/metadata/iptc/Iso2022Converter.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,5 +28,5 @@
 
 /**
- * Provides a human-readable string version of the tag stored in a HuffmanTableDirectory.
+ * Provides a human-readable string version of the tag stored in a {@link HuffmanTablesDirectory}.
  *
  * <ul>
@@ -61,5 +61,5 @@
     {
         Integer value = _directory.getInteger(TAG_NUMBER_OF_TABLES);
-        if (value==null)
+        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 15216)
+++ trunk/src/com/drew/metadata/jpeg/HuffmanTablesDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,4 +25,5 @@
 import java.util.HashMap;
 import java.util.List;
+
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
@@ -168,5 +169,5 @@
     /**
      * @return The {@link List} of {@link HuffmanTable}s in this
-     *         {@link Directory}.
+     * {@link Directory}.
      */
     @NotNull
@@ -227,22 +228,27 @@
      */
     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,
+        private final int _tableLength;
+        private final HuffmanTableClass _tableClass;
+        private final int _tableDestinationId;
+        private final byte[] _lengthBytes;
+        private final byte[] _valueBytes;
+
+        @SuppressWarnings("ConstantConditions")
+        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;
+            @NotNull byte[] lengthBytes,
+            @NotNull byte[] valueBytes)
+        {
+            if (lengthBytes == null)
+                throw new IllegalArgumentException("lengthBytes cannot be null.");
+            if (valueBytes == null)
+                throw new IllegalArgumentException("valueBytes cannot be null.");
+
+            _tableClass = tableClass;
+            _tableDestinationId = tableDestinationId;
+            _lengthBytes = lengthBytes;
+            _valueBytes = valueBytes;
+            _tableLength = _valueBytes.length + 17;
         }
 
@@ -251,7 +257,6 @@
          */
         public int getTableLength() {
-            return tableLength;
-        }
-
+            return _tableLength;
+        }
 
         /**
@@ -259,7 +264,6 @@
          */
         public HuffmanTableClass getTableClass() {
-            return tableClass;
-        }
-
+            return _tableClass;
+        }
 
         /**
@@ -267,28 +271,24 @@
          */
         public int getTableDestinationId() {
-            return tableDestinationId;
-        }
-
+            return _tableDestinationId;
+        }
 
         /**
          * @return A byte array with the L values for this table.
          */
+        @NotNull
         public byte[] getLengthBytes() {
-            if (lengthBytes == null)
-                return null;
-            byte[] result = new byte[lengthBytes.length];
-            System.arraycopy(lengthBytes, 0, result, 0, lengthBytes.length);
+            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.
          */
+        @NotNull
         public byte[] getValueBytes() {
-            if (valueBytes == null)
-                return null;
-            byte[] result = new byte[valueBytes.length];
-            System.arraycopy(valueBytes, 0, result, 0, valueBytes.length);
+            byte[] result = new byte[_valueBytes.length];
+            System.arraycopy(_valueBytes, 0, result, 0, _valueBytes.length);
             return result;
         }
@@ -318,16 +318,16 @@
          */
         public boolean isTypical() {
-            if (tableClass == HuffmanTableClass.DC) {
+            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) {
+                    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);
+                    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;
@@ -351,7 +351,10 @@
             public static HuffmanTableClass typeOf(int value) {
                 switch (value) {
-                    case 0: return DC;
-                    case 1 : return AC;
-                    default: return UNKNOWN;
+                    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 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegComponent.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegDhtReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDhtReader.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegDhtReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/jpeg/JpegDnlReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDnlReader.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegDnlReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,5 +30,4 @@
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
 
Index: trunk/src/com/drew/metadata/jpeg/JpegReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 15216)
+++ trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 15216)
+++ trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 15217)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2017 Drew Noakes
+ * Copyright 2002-2019 Drew Noakes and contributors
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,4 +24,5 @@
 import com.drew.lang.Rational;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.ErrorDirectory;
@@ -40,10 +41,12 @@
     private final Stack<Directory> _directoryStack = new Stack<Directory>();
 
-    protected Directory _currentDirectory;
+    @Nullable private Directory _rootParentDirectory;
+    @Nullable protected Directory _currentDirectory;
     protected final Metadata _metadata;
 
-    protected DirectoryTiffHandler(Metadata metadata)
+    protected DirectoryTiffHandler(Metadata metadata, @Nullable Directory parentDirectory)
     {
         _metadata = metadata;
+        _rootParentDirectory = parentDirectory;
     }
 
@@ -55,5 +58,5 @@
     protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
     {
-        Directory newDirectory = null;
+        Directory newDirectory;
 
         try {
@@ -65,15 +68,20 @@
         }
 
-        if (newDirectory != null)
-        {
-            // If this is the first directory, don't add to the stack
-            if (_currentDirectory != null)
-            {
-                _directoryStack.push(_currentDirectory);
-                newDirectory.setParent(_currentDirectory);
+        // If this is the first directory, don't add to the stack
+        if (_currentDirectory == null) {
+            // Apply any pending root parent to this new directory
+            if (_rootParentDirectory != null) {
+                newDirectory.setParent(_rootParentDirectory);
+                _rootParentDirectory = null;
             }
-            _currentDirectory = newDirectory;
-            _metadata.addDirectory(_currentDirectory);
         }
+        else {
+            // The current directory is pushed onto the stack, and set as the new directory's parent
+            _directoryStack.push(_currentDirectory);
+            newDirectory.setParent(_currentDirectory);
+        }
+
+        _currentDirectory = newDirectory;
+        _metadata.addDirectory(_currentDirectory);
     }
 
