[josm] 14/28: Imported Upstream version 0.0.svn8800+dfsg2

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Fri Oct 9 22:27:03 UTC 2015


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository josm.

commit 3f226491e1f5c63d77d09c577aa54c21d54ed97d
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Fri Oct 9 15:24:05 2015 +0200

    Imported Upstream version 0.0.svn8800+dfsg2
---
 src/com/drew/imaging/ImageProcessingException.java |   49 +
 src/com/drew/imaging/PhotographicConversions.java  |   61 ++
 src/com/drew/imaging/jpeg/JpegMetadataReader.java  |  135 +++
 .../drew/imaging/jpeg/JpegProcessingException.java |   49 +
 src/com/drew/imaging/jpeg/JpegSegmentData.java     |  270 +++++
 .../imaging/jpeg/JpegSegmentMetadataReader.java    |   26 +
 src/com/drew/imaging/jpeg/JpegSegmentReader.java   |  168 +++
 src/com/drew/imaging/jpeg/JpegSegmentType.java     |  174 +++
 src/com/drew/imaging/jpeg/package.html             |   33 +
 src/com/drew/imaging/package.html                  |   33 +
 src/com/drew/imaging/tiff/TiffDataFormat.java      |  107 ++
 src/com/drew/imaging/tiff/TiffHandler.java         |   85 ++
 .../drew/imaging/tiff/TiffProcessingException.java |   51 +
 src/com/drew/imaging/tiff/TiffReader.java          |  368 +++++++
 src/com/drew/imaging/tiff/package.html             |   33 +
 src/com/drew/lang/BufferBoundsException.java       |   59 ++
 src/com/drew/lang/ByteArrayReader.java             |   89 ++
 src/com/drew/lang/CompoundException.java           |  109 ++
 src/com/drew/lang/GeoLocation.java                 |  163 +++
 src/com/drew/lang/NullOutputStream.java            |   43 +
 src/com/drew/lang/RandomAccessReader.java          |  407 +++++++
 src/com/drew/lang/Rational.java                    |  296 ++++++
 src/com/drew/lang/SequentialByteArrayReader.java   |  108 ++
 src/com/drew/lang/SequentialReader.java            |  308 ++++++
 src/com/drew/lang/StreamReader.java                |  114 ++
 src/com/drew/lang/StringUtil.java                  |  115 ++
 src/com/drew/lang/annotations/NotNull.java         |   29 +
 src/com/drew/lang/annotations/Nullable.java        |   29 +
 .../drew/lang/annotations/SuppressWarnings.java    |   42 +
 src/com/drew/lang/annotations/package.html         |   34 +
 src/com/drew/lang/package.html                     |   33 +
 src/com/drew/metadata/Age.java                     |  173 +++
 src/com/drew/metadata/DefaultTagDescriptor.java    |   51 +
 src/com/drew/metadata/Directory.java               |  958 +++++++++++++++++
 src/com/drew/metadata/Face.java                    |  133 +++
 src/com/drew/metadata/Metadata.java                |  210 ++++
 src/com/drew/metadata/MetadataException.java       |   49 +
 src/com/drew/metadata/MetadataReader.java          |   42 +
 src/com/drew/metadata/Tag.java                     |  132 +++
 src/com/drew/metadata/TagDescriptor.java           |  274 +++++
 src/com/drew/metadata/exif/ExifDescriptorBase.java | 1105 ++++++++++++++++++++
 src/com/drew/metadata/exif/ExifDirectoryBase.java  |  732 +++++++++++++
 src/com/drew/metadata/exif/ExifIFD0Descriptor.java |   37 +
 src/com/drew/metadata/exif/ExifIFD0Directory.java  |   67 ++
 .../drew/metadata/exif/ExifInteropDescriptor.java  |   36 +
 .../drew/metadata/exif/ExifInteropDirectory.java   |   60 ++
 src/com/drew/metadata/exif/ExifReader.java         |  101 ++
 .../drew/metadata/exif/ExifSubIFDDescriptor.java   |   36 +
 .../drew/metadata/exif/ExifSubIFDDirectory.java    |   63 ++
 .../metadata/exif/ExifThumbnailDescriptor.java     |  109 ++
 .../drew/metadata/exif/ExifThumbnailDirectory.java |  335 ++++++
 src/com/drew/metadata/exif/ExifTiffHandler.java    |  352 +++++++
 src/com/drew/metadata/exif/GpsDescriptor.java      |  241 +++++
 src/com/drew/metadata/exif/GpsDirectory.java       |  188 ++++
 .../exif/makernotes/CanonMakernoteDescriptor.java  |  723 +++++++++++++
 .../exif/makernotes/CanonMakernoteDirectory.java   |  723 +++++++++++++
 .../makernotes/CasioType1MakernoteDescriptor.java  |  204 ++++
 .../makernotes/CasioType1MakernoteDirectory.java   |  104 ++
 .../makernotes/CasioType2MakernoteDescriptor.java  |  330 ++++++
 .../makernotes/CasioType2MakernoteDirectory.java   |  232 ++++
 .../makernotes/FujifilmMakernoteDescriptor.java    |  468 +++++++++
 .../makernotes/FujifilmMakernoteDirectory.java     |  178 ++++
 .../exif/makernotes/KodakMakernoteDescriptor.java  |  151 +++
 .../exif/makernotes/KodakMakernoteDirectory.java   |  113 ++
 .../makernotes/KyoceraMakernoteDescriptor.java     |   73 ++
 .../exif/makernotes/KyoceraMakernoteDirectory.java |   65 ++
 .../exif/makernotes/LeicaMakernoteDescriptor.java  |  127 +++
 .../exif/makernotes/LeicaMakernoteDirectory.java   |  107 ++
 .../makernotes/NikonType1MakernoteDescriptor.java  |  169 +++
 .../makernotes/NikonType1MakernoteDirectory.java   |   92 ++
 .../makernotes/NikonType2MakernoteDescriptor.java  |  359 +++++++
 .../makernotes/NikonType2MakernoteDirectory.java   |  924 ++++++++++++++++
 .../makernotes/OlympusMakernoteDescriptor.java     |  749 +++++++++++++
 .../exif/makernotes/OlympusMakernoteDirectory.java |  478 +++++++++
 .../makernotes/PanasonicMakernoteDescriptor.java   |  690 ++++++++++++
 .../makernotes/PanasonicMakernoteDirectory.java    |  644 ++++++++++++
 .../exif/makernotes/PentaxMakernoteDescriptor.java |  159 +++
 .../exif/makernotes/PentaxMakernoteDirectory.java  |  170 +++
 .../exif/makernotes/RicohMakernoteDescriptor.java  |   67 ++
 .../exif/makernotes/RicohMakernoteDirectory.java   |   69 ++
 .../exif/makernotes/SanyoMakernoteDescriptor.java  |  228 ++++
 .../exif/makernotes/SanyoMakernoteDirectory.java   |  124 +++
 .../exif/makernotes/SigmaMakernoteDescriptor.java  |   84 ++
 .../exif/makernotes/SigmaMakernoteDirectory.java   |  109 ++
 .../makernotes/SonyType1MakernoteDescriptor.java   |  677 ++++++++++++
 .../makernotes/SonyType1MakernoteDirectory.java    |  228 ++++
 .../makernotes/SonyType6MakernoteDescriptor.java   |   59 ++
 .../makernotes/SonyType6MakernoteDirectory.java    |   70 ++
 src/com/drew/metadata/exif/makernotes/package.html |   33 +
 src/com/drew/metadata/exif/package.html            |   33 +
 .../drew/metadata/file/FileMetadataDescriptor.java |   60 ++
 .../drew/metadata/file/FileMetadataDirectory.java  |   64 ++
 src/com/drew/metadata/file/FileMetadataReader.java |   29 +
 src/com/drew/metadata/file/package.html            |   34 +
 src/com/drew/metadata/iptc/IptcDescriptor.java     |  263 +++++
 src/com/drew/metadata/iptc/IptcDirectory.java      |  238 +++++
 src/com/drew/metadata/iptc/IptcReader.java         |  240 +++++
 src/com/drew/metadata/iptc/Iso2022Converter.java   |   83 ++
 src/com/drew/metadata/iptc/package.html            |   33 +
 .../drew/metadata/jpeg/JpegCommentDescriptor.java  |   44 +
 .../drew/metadata/jpeg/JpegCommentDirectory.java   |   66 ++
 src/com/drew/metadata/jpeg/JpegCommentReader.java  |   60 ++
 src/com/drew/metadata/jpeg/JpegComponent.java      |   90 ++
 src/com/drew/metadata/jpeg/JpegDescriptor.java     |  132 +++
 src/com/drew/metadata/jpeg/JpegDirectory.java      |  126 +++
 src/com/drew/metadata/jpeg/JpegReader.java         |  104 ++
 src/com/drew/metadata/jpeg/package.html            |   33 +
 src/com/drew/metadata/package.html                 |   33 +
 .../drew/metadata/tiff/DirectoryTiffHandler.java   |  192 ++++
 src/com/drew/metadata/tiff/package.html            |   34 +
 110 files changed, 20875 insertions(+)

diff --git a/src/com/drew/imaging/ImageProcessingException.java b/src/com/drew/imaging/ImageProcessingException.java
new file mode 100755
index 0000000..ebfa440
--- /dev/null
+++ b/src/com/drew/imaging/ImageProcessingException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging;
+
+import com.drew.lang.CompoundException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An exception class thrown upon an unexpected condition that was fatal for the processing of an image.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ImageProcessingException extends CompoundException
+{
+    private static final long serialVersionUID = -9115669182209912676L;
+
+    public ImageProcessingException(@Nullable String message)
+    {
+        super(message);
+    }
+
+    public ImageProcessingException(@Nullable String message, @Nullable Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public ImageProcessingException(@Nullable Throwable cause)
+    {
+        super(cause);
+    }
+}
diff --git a/src/com/drew/imaging/PhotographicConversions.java b/src/com/drew/imaging/PhotographicConversions.java
new file mode 100644
index 0000000..913acac
--- /dev/null
+++ b/src/com/drew/imaging/PhotographicConversions.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging;
+
+/**
+ * Contains helper methods that perform photographic conversions.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class PhotographicConversions
+{
+    public final static double ROOT_TWO = Math.sqrt(2);
+
+    private PhotographicConversions() throws Exception
+    {
+        throw new Exception("Not intended for instantiation.");
+    }
+
+    /**
+     * Converts an aperture value to its corresponding F-stop number.
+     *
+     * @param aperture the aperture value to convert
+     * @return the F-stop number of the specified aperture
+     */
+    public static double apertureToFStop(double aperture)
+    {
+        return Math.pow(ROOT_TWO, aperture);
+
+        // NOTE jhead uses a different calculation as far as i can tell...  this confuses me...
+        // fStop = (float)Math.exp(aperture * Math.log(2) * 0.5));
+    }
+
+    /**
+     * Converts a shutter speed to an exposure time.
+     *
+     * @param shutterSpeed the shutter speed to convert
+     * @return the exposure time of the specified shutter speed
+     */
+    public static double shutterSpeedToExposureTime(double shutterSpeed)
+    {
+        return (float) (1 / Math.exp(shutterSpeed * Math.log(2)));
+    }
+}
diff --git a/src/com/drew/imaging/jpeg/JpegMetadataReader.java b/src/com/drew/imaging/jpeg/JpegMetadataReader.java
new file mode 100644
index 0000000..31ee48a
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/JpegMetadataReader.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.jpeg;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.drew.lang.StreamReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Metadata;
+//import com.drew.metadata.adobe.AdobeJpegReader;
+import com.drew.metadata.exif.ExifReader;
+import com.drew.metadata.file.FileMetadataReader;
+//import com.drew.metadata.icc.IccReader;
+import com.drew.metadata.iptc.IptcReader;
+//import com.drew.metadata.jfif.JfifReader;
+import com.drew.metadata.jpeg.JpegCommentReader;
+import com.drew.metadata.jpeg.JpegReader;
+//import com.drew.metadata.photoshop.PhotoshopReader;
+//import com.drew.metadata.xmp.XmpReader;
+
+/**
+ * Obtains all available metadata from JPEG formatted files.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegMetadataReader
+{
+    public static final Iterable<JpegSegmentMetadataReader> ALL_READERS = Arrays.asList(
+            new JpegReader(),
+            new JpegCommentReader(),
+            //new JfifReader(),
+            new ExifReader(),
+            //new XmpReader(),
+            //new IccReader(),
+            //new PhotoshopReader(),
+            new IptcReader()//,
+            //new AdobeJpegReader()
+    );
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
+    {
+        Metadata metadata = new Metadata();
+        process(metadata, inputStream, readers);
+        return metadata;
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException, IOException
+    {
+        return readMetadata(inputStream, null);
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
+    {
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
+        try {
+            metadata = readMetadata(inputStream, readers);
+        } finally {
+            inputStream.close();
+        }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file) throws JpegProcessingException, IOException
+    {
+        return readMetadata(file, null);
+    }
+
+    public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream) throws JpegProcessingException, IOException
+    {
+        process(metadata, inputStream, null);
+    }
+
+    public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
+    {
+        if (readers == null)
+            readers = ALL_READERS;
+
+        Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>();
+        for (JpegSegmentMetadataReader reader : readers) {
+            for (JpegSegmentType type : reader.getSegmentTypes()) {
+                segmentTypes.add(type);
+            }
+        }
+
+        JpegSegmentData segmentData = JpegSegmentReader.readSegments(new StreamReader(inputStream), segmentTypes);
+
+        processJpegSegmentData(metadata, readers, segmentData);
+    }
+
+    public static void processJpegSegmentData(Metadata metadata, Iterable<JpegSegmentMetadataReader> readers, JpegSegmentData segmentData)
+    {
+        // Pass the appropriate byte arrays to each reader.
+        for (JpegSegmentMetadataReader reader : readers) {
+            for (JpegSegmentType segmentType : reader.getSegmentTypes()) {
+                reader.readJpegSegments(segmentData.getSegments(segmentType), metadata, segmentType);
+            }
+        }
+    }
+
+    private JpegMetadataReader() throws Exception
+    {
+        throw new Exception("Not intended for instantiation");
+    }
+}
diff --git a/src/com/drew/imaging/jpeg/JpegProcessingException.java b/src/com/drew/imaging/jpeg/JpegProcessingException.java
new file mode 100644
index 0000000..78fd61a
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/JpegProcessingException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.jpeg;
+
+import com.drew.imaging.ImageProcessingException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An exception class thrown upon unexpected and fatal conditions while processing a JPEG file.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegProcessingException extends ImageProcessingException
+{
+    private static final long serialVersionUID = -7870179776125450158L;
+
+    public JpegProcessingException(@Nullable String message)
+    {
+        super(message);
+    }
+
+    public JpegProcessingException(@Nullable String message, @Nullable Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public JpegProcessingException(@Nullable Throwable cause)
+    {
+        super(cause);
+    }
+}
diff --git a/src/com/drew/imaging/jpeg/JpegSegmentData.java b/src/com/drew/imaging/jpeg/JpegSegmentData.java
new file mode 100644
index 0000000..70695e5
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/JpegSegmentData.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.util.*;
+
+/**
+ * Holds a collection of JPEG data segments.  This need not necessarily be all segments
+ * within the JPEG. For example, it may be convenient to store only the non-image
+ * segments when analysing metadata.
+ * <p>
+ * Segments are keyed via their {@link JpegSegmentType}. Where multiple segments use the
+ * same segment type, they will all be stored and available.
+ * <p>
+ * Each segment type may contain multiple entries. Conceptually the model is:
+ * <code>Map<JpegSegmentType, Collection<byte[]>></code>. This class provides
+ * convenience methods around that structure.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegSegmentData
+{
+    // TODO key this on JpegSegmentType rather than Byte, and hopefully lose much of the use of 'byte' with this class
+    @NotNull
+    private final HashMap<Byte, List<byte[]>> _segmentDataMap = new HashMap<Byte, List<byte[]>>(10);
+
+    /**
+     * Adds segment bytes to the collection.
+     *
+     * @param segmentType  the type of the segment being added
+     * @param segmentBytes the byte array holding data for the segment being added
+     */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
+    public void addSegment(byte segmentType, @NotNull byte[] segmentBytes)
+    {
+        getOrCreateSegmentList(segmentType).add(segmentBytes);
+    }
+
+    /**
+     * Gets the set of JPEG segment type identifiers.
+     */
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>();
+
+        for (Byte segmentTypeByte : _segmentDataMap.keySet())
+        {
+            JpegSegmentType segmentType = JpegSegmentType.fromByte(segmentTypeByte);
+            if (segmentType == null) {
+                throw new IllegalStateException("Should not have a segmentTypeByte that is not in the enum: " + Integer.toHexString(segmentTypeByte));
+            }
+            segmentTypes.add(segmentType);
+        }
+
+        return segmentTypes;
+    }
+
+    /**
+     * Gets the first JPEG segment data for the specified type.
+     *
+     * @param segmentType the JpegSegmentType for the desired segment
+     * @return a byte[] containing segment data or null if no data exists for that segment
+     */
+    @Nullable
+    public byte[] getSegment(byte segmentType)
+    {
+        return getSegment(segmentType, 0);
+    }
+
+    /**
+     * Gets the first JPEG segment data for the specified type.
+     *
+     * @param segmentType the JpegSegmentType for the desired segment
+     * @return a byte[] containing segment data or null if no data exists for that segment
+     */
+    @Nullable
+    public byte[] getSegment(@NotNull JpegSegmentType segmentType)
+    {
+        return getSegment(segmentType.byteValue, 0);
+    }
+
+    /**
+     * Gets segment data for a specific occurrence and type.  Use this method when more than one occurrence
+     * of segment data for a given type exists.
+     *
+     * @param segmentType identifies the required segment
+     * @param occurrence  the zero-based index of the occurrence
+     * @return the segment data as a byte[], or null if no segment exists for the type & occurrence
+     */
+    @Nullable
+    public byte[] getSegment(@NotNull JpegSegmentType segmentType, int occurrence)
+    {
+        return getSegment(segmentType.byteValue, occurrence);
+    }
+
+    /**
+     * Gets segment data for a specific occurrence and type.  Use this method when more than one occurrence
+     * of segment data for a given type exists.
+     *
+     * @param segmentType identifies the required segment
+     * @param occurrence  the zero-based index of the occurrence
+     * @return the segment data as a byte[], or null if no segment exists for the type & occurrence
+     */
+    @Nullable
+    public byte[] getSegment(byte segmentType, int occurrence)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentType);
+
+        return segmentList != null && segmentList.size() > occurrence
+                ? segmentList.get(occurrence)
+                : null;
+    }
+
+    /**
+     * Returns all instances of a given JPEG segment.  If no instances exist, an empty sequence is returned.
+     *
+     * @param segmentType a number which identifies the type of JPEG segment being queried
+     * @return zero or more byte arrays, each holding the data of a JPEG segment
+     */
+    @NotNull
+    public Iterable<byte[]> getSegments(@NotNull JpegSegmentType segmentType)
+    {
+        return getSegments(segmentType.byteValue);
+    }
+
+    /**
+     * Returns all instances of a given JPEG segment.  If no instances exist, an empty sequence is returned.
+     *
+     * @param segmentType a number which identifies the type of JPEG segment being queried
+     * @return zero or more byte arrays, each holding the data of a JPEG segment
+     */
+    @NotNull
+    public Iterable<byte[]> getSegments(byte segmentType)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentType);
+        return segmentList == null ? new ArrayList<byte[]>() : segmentList;
+    }
+
+    @Nullable
+    private List<byte[]> getSegmentList(byte segmentType)
+    {
+        return _segmentDataMap.get(segmentType);
+    }
+
+    @NotNull
+    private List<byte[]> getOrCreateSegmentList(byte segmentType)
+    {
+        List<byte[]> segmentList;
+        if (_segmentDataMap.containsKey(segmentType)) {
+            segmentList = _segmentDataMap.get(segmentType);
+        } else {
+            segmentList = new ArrayList<byte[]>();
+            _segmentDataMap.put(segmentType, segmentList);
+        }
+        return segmentList;
+    }
+
+    /**
+     * Returns the count of segment data byte arrays stored for a given segment type.
+     *
+     * @param segmentType identifies the required segment
+     * @return the segment count (zero if no segments exist).
+     */
+    public int getSegmentCount(@NotNull JpegSegmentType segmentType)
+    {
+        return getSegmentCount(segmentType.byteValue);
+    }
+
+    /**
+     * Returns the count of segment data byte arrays stored for a given segment type.
+     *
+     * @param segmentType identifies the required segment
+     * @return the segment count (zero if no segments exist).
+     */
+    public int getSegmentCount(byte segmentType)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentType);
+        return segmentList == null ? 0 : segmentList.size();
+    }
+
+    /**
+     * Removes a specified instance of a segment's data from the collection.  Use this method when more than one
+     * occurrence of segment data exists for a given type exists.
+     *
+     * @param segmentType identifies the required segment
+     * @param occurrence  the zero-based index of the segment occurrence to remove.
+     */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
+    public void removeSegmentOccurrence(@NotNull JpegSegmentType segmentType, int occurrence)
+    {
+        removeSegmentOccurrence(segmentType.byteValue, occurrence);
+    }
+
+    /**
+     * Removes a specified instance of a segment's data from the collection.  Use this method when more than one
+     * occurrence of segment data exists for a given type exists.
+     *
+     * @param segmentType identifies the required segment
+     * @param occurrence  the zero-based index of the segment occurrence to remove.
+     */
+    @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
+    public void removeSegmentOccurrence(byte segmentType, int occurrence)
+    {
+        final List<byte[]> segmentList = _segmentDataMap.get(segmentType);
+        segmentList.remove(occurrence);
+    }
+
+    /**
+     * Removes all segments from the collection having the specified type.
+     *
+     * @param segmentType identifies the required segment
+     */
+    public void removeSegment(@NotNull JpegSegmentType segmentType)
+    {
+        removeSegment(segmentType.byteValue);
+    }
+
+    /**
+     * Removes all segments from the collection having the specified type.
+     *
+     * @param segmentType identifies the required segment
+     */
+    public void removeSegment(byte segmentType)
+    {
+        _segmentDataMap.remove(segmentType);
+    }
+
+    /**
+     * Determines whether data is present for a given segment type.
+     *
+     * @param segmentType identifies the required segment
+     * @return true if data exists, otherwise false
+     */
+    public boolean containsSegment(@NotNull JpegSegmentType segmentType)
+    {
+        return containsSegment(segmentType.byteValue);
+    }
+
+    /**
+     * Determines whether data is present for a given segment type.
+     *
+     * @param segmentType identifies the required segment
+     * @return true if data exists, otherwise false
+     */
+    public boolean containsSegment(byte segmentType)
+    {
+        return _segmentDataMap.containsKey(segmentType);
+    }
+}
diff --git a/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java b/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java
new file mode 100644
index 0000000..53338b1
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java
@@ -0,0 +1,26 @@
+package com.drew.imaging.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+/**
+ * Defines an object that extracts metadata from in JPEG segments.
+ */
+public interface JpegSegmentMetadataReader
+{
+    /**
+     * Gets the set of JPEG segment types that this reader is interested in.
+     */
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes();
+
+    /**
+     * Extracts metadata from all instances of a particular JPEG segment type.
+     *
+     * @param segments A sequence of byte arrays from which the metadata should be extracted. These are in the order
+     *                 encountered in the original file.
+     * @param metadata The {@link Metadata} object into which extracted values should be merged.
+     * @param segmentType The {@link JpegSegmentType} being read.
+     */
+    public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType);
+}
diff --git a/src/com/drew/imaging/jpeg/JpegSegmentReader.java b/src/com/drew/imaging/jpeg/JpegSegmentReader.java
new file mode 100644
index 0000000..d8e6876
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/JpegSegmentReader.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.jpeg;
+
+import com.drew.lang.SequentialReader;
+import com.drew.lang.StreamReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Performs read functions of JPEG files, returning specific file segments.
+ * <p>
+ * JPEG files are composed of a sequence of consecutive JPEG 'segments'. Each is identified by one of a set of byte
+ * values, modelled in the {@link JpegSegmentType} enumeration. Use <code>readSegments</code> to read out the some
+ * or all segments into a {@link JpegSegmentData} object, from which the raw JPEG segment byte arrays may be accessed.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegSegmentReader
+{
+    /**
+     * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet).
+     */
+    private static final byte SEGMENT_SOS = (byte) 0xDA;
+
+    /**
+     * Private, because one wouldn't search for it.
+     */
+    private static final byte MARKER_EOI = (byte) 0xD9;
+
+    /**
+     * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object.
+     * <p>
+     * Will not return SOS (start of scan) or EOI (end of image) segments.
+     *
+     * @param file a {@link File} from which the JPEG data will be read.
+     * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code>
+     *                     then all found segment types are returned.
+     */
+    @NotNull
+    public static JpegSegmentData readSegments(@NotNull File file, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException
+    {
+        FileInputStream stream = null;
+        try {
+            stream = new FileInputStream(file);
+            return readSegments(new StreamReader(stream), segmentTypes);
+        } finally {
+            if (stream != null) {
+                stream.close();
+            }
+        }
+    }
+
+    /**
+     * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object.
+     * <p>
+     * Will not return SOS (start of scan) or EOI (end of image) segments.
+     *
+     * @param reader a {@link SequentialReader} from which the JPEG data will be read. It must be positioned at the
+     *               beginning of the JPEG data stream.
+     * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code>
+     *                     then all found segment types are returned.
+     */
+    @NotNull
+    public static JpegSegmentData readSegments(@NotNull final SequentialReader reader, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException
+    {
+        // Must be big-endian
+        assert (reader.isMotorolaByteOrder());
+
+        // first two bytes should be JPEG magic number
+        final int magicNumber = reader.getUInt16();
+        if (magicNumber != 0xFFD8) {
+            throw new JpegProcessingException("JPEG data is expected to begin with 0xFFD8 (ÿØ) not 0x" + Integer.toHexString(magicNumber));
+        }
+
+        Set<Byte> segmentTypeBytes = null;
+        if (segmentTypes != null) {
+            segmentTypeBytes = new HashSet<Byte>();
+            for (JpegSegmentType segmentType : segmentTypes) {
+                segmentTypeBytes.add(segmentType.byteValue);
+            }
+        }
+
+        JpegSegmentData segmentData = new JpegSegmentData();
+
+        do {
+            // Find the segment marker. Markers are zero or more 0xFF bytes, followed
+            // by a 0xFF and then a byte not equal to 0x00 or 0xFF.
+
+            final short segmentIdentifier = reader.getUInt8();
+
+            // We must have at least one 0xFF byte
+            if (segmentIdentifier != 0xFF)
+                throw new JpegProcessingException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase());
+
+            // Read until we have a non-0xFF byte. This identifies the segment type.
+            byte segmentType = reader.getInt8();
+            while (segmentType == (byte)0xFF)
+                segmentType = reader.getInt8();
+
+            if (segmentType == 0)
+                throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier");
+
+            if (segmentType == SEGMENT_SOS) {
+                // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
+                // have to search for the two bytes: 0xFF 0xD9 (EOI).
+                // It comes last so simply return at this point
+                return segmentData;
+            }
+
+            if (segmentType == MARKER_EOI) {
+                // the 'End-Of-Image' segment -- this should never be found in this fashion
+                return segmentData;
+            }
+
+            // next 2-bytes are <segment-size>: [high-byte] [low-byte]
+            int segmentLength = reader.getUInt16();
+
+            // segment length includes size bytes, so subtract two
+            segmentLength -= 2;
+
+            if (segmentLength < 0)
+                throw new JpegProcessingException("JPEG segment size would be less than zero");
+
+            // Check whether we are interested in this segment
+            if (segmentTypeBytes == null || segmentTypeBytes.contains(segmentType)) {
+                byte[] segmentBytes = reader.getBytes(segmentLength);
+                assert (segmentLength == segmentBytes.length);
+                segmentData.addSegment(segmentType, segmentBytes);
+            } else {
+                // Some if the JPEG is truncated, just return what data we've already gathered
+                if (!reader.trySkip(segmentLength)) {
+                    return segmentData;
+                }
+            }
+
+        } while (true);
+    }
+
+    private JpegSegmentReader() throws Exception
+    {
+        throw new Exception("Not intended for instantiation.");
+    }
+}
diff --git a/src/com/drew/imaging/jpeg/JpegSegmentType.java b/src/com/drew/imaging/jpeg/JpegSegmentType.java
new file mode 100644
index 0000000..d76414e
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/JpegSegmentType.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.jpeg;
+
+import com.drew.lang.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An enumeration of the known segment types found in JPEG files.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public enum JpegSegmentType
+{
+    /** APP0 JPEG segment identifier -- JFIF data (also JFXX apparently). */
+    APP0((byte)0xE0, true),
+
+    /** APP1 JPEG segment identifier -- where Exif data is kept.  XMP data is also kept in here, though usually in a second instance. */
+    APP1((byte)0xE1, true),
+
+    /** APP2 JPEG segment identifier. */
+    APP2((byte)0xE2, true),
+
+    /** APP3 JPEG segment identifier. */
+    APP3((byte)0xE3, true),
+
+    /** APP4 JPEG segment identifier. */
+    APP4((byte)0xE4, true),
+
+    /** APP5 JPEG segment identifier. */
+    APP5((byte)0xE5, true),
+
+    /** APP6 JPEG segment identifier. */
+    APP6((byte)0xE6, true),
+
+    /** APP7 JPEG segment identifier. */
+    APP7((byte)0xE7, true),
+
+    /** APP8 JPEG segment identifier. */
+    APP8((byte)0xE8, true),
+
+    /** APP9 JPEG segment identifier. */
+    APP9((byte)0xE9, true),
+
+    /** APPA (App10) JPEG segment identifier -- can hold Unicode comments. */
+    APPA((byte)0xEA, true),
+
+    /** APPB (App11) JPEG segment identifier. */
+    APPB((byte)0xEB, true),
+
+    /** APPC (App12) JPEG segment identifier. */
+    APPC((byte)0xEC, true),
+
+    /** APPD (App13) JPEG segment identifier -- IPTC data in here. */
+    APPD((byte)0xED, true),
+
+    /** APPE (App14) JPEG segment identifier. */
+    APPE((byte)0xEE, true),
+
+    /** APPF (App15) JPEG segment identifier. */
+    APPF((byte)0xEF, true),
+
+    /** Start Of Image segment identifier. */
+    SOI((byte)0xD8, false),
+
+    /** Define Quantization Table segment identifier. */
+    DQT((byte)0xDB, false),
+
+    /** Define Huffman Table segment identifier. */
+    DHT((byte)0xC4, false),
+
+    /** Start-of-Frame (0) segment identifier. */
+    SOF0((byte)0xC0, true),
+
+    /** Start-of-Frame (1) segment identifier. */
+    SOF1((byte)0xC1, true),
+
+    /** Start-of-Frame (2) segment identifier. */
+    SOF2((byte)0xC2, true),
+
+    /** Start-of-Frame (3) segment identifier. */
+    SOF3((byte)0xC3, true),
+
+//    /** Start-of-Frame (4) segment identifier. */
+//    SOF4((byte)0xC4, true),
+
+    /** Start-of-Frame (5) segment identifier. */
+    SOF5((byte)0xC5, true),
+
+    /** Start-of-Frame (6) segment identifier. */
+    SOF6((byte)0xC6, true),
+
+    /** Start-of-Frame (7) segment identifier. */
+    SOF7((byte)0xC7, true),
+
+    /** Start-of-Frame (8) segment identifier. */
+    SOF8((byte)0xC8, true),
+
+    /** Start-of-Frame (9) segment identifier. */
+    SOF9((byte)0xC9, true),
+
+    /** Start-of-Frame (10) segment identifier. */
+    SOF10((byte)0xCA, true),
+
+    /** Start-of-Frame (11) segment identifier. */
+    SOF11((byte)0xCB, true),
+
+//    /** Start-of-Frame (12) segment identifier. */
+//    SOF12((byte)0xCC, true),
+
+    /** Start-of-Frame (13) segment identifier. */
+    SOF13((byte)0xCD, true),
+
+    /** Start-of-Frame (14) segment identifier. */
+    SOF14((byte)0xCE, true),
+
+    /** Start-of-Frame (15) segment identifier. */
+    SOF15((byte)0xCF, true),
+
+    /** JPEG comment segment identifier. */
+    COM((byte)0xFE, true);
+
+    public static final Collection<JpegSegmentType> canContainMetadataTypes;
+
+    static {
+        List<JpegSegmentType> segmentTypes = new ArrayList<JpegSegmentType>();
+        for (JpegSegmentType segmentType : JpegSegmentType.class.getEnumConstants()) {
+            if (segmentType.canContainMetadata) {
+                segmentTypes.add(segmentType);
+            }
+        }
+        canContainMetadataTypes = segmentTypes;
+    }
+
+    public final byte byteValue;
+    public final boolean canContainMetadata;
+
+    JpegSegmentType(byte byteValue, boolean canContainMetadata)
+    {
+        this.byteValue = byteValue;
+        this.canContainMetadata = canContainMetadata;
+    }
+
+    @Nullable
+    public static JpegSegmentType fromByte(byte segmentTypeByte)
+    {
+        for (JpegSegmentType segmentType : JpegSegmentType.class.getEnumConstants()) {
+            if (segmentType.byteValue == segmentTypeByte)
+                return segmentType;
+        }
+        return null;
+    }
+}
diff --git a/src/com/drew/imaging/jpeg/package.html b/src/com/drew/imaging/jpeg/package.html
new file mode 100644
index 0000000..d65ff55
--- /dev/null
+++ b/src/com/drew/imaging/jpeg/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for working with JPEG files.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/imaging/package.html b/src/com/drew/imaging/package.html
new file mode 100644
index 0000000..af33269
--- /dev/null
+++ b/src/com/drew/imaging/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for working with image file formats and photographic conversions.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/imaging/tiff/TiffDataFormat.java b/src/com/drew/imaging/tiff/TiffDataFormat.java
new file mode 100644
index 0000000..765cd71
--- /dev/null
+++ b/src/com/drew/imaging/tiff/TiffDataFormat.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.tiff;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An enumeration of data formats used by the TIFF specification.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class TiffDataFormat
+{
+    public static final int CODE_INT8_U = 1;
+    public static final int CODE_STRING = 2;
+    public static final int CODE_INT16_U = 3;
+    public static final int CODE_INT32_U = 4;
+    public static final int CODE_RATIONAL_U = 5;
+    public static final int CODE_INT8_S = 6;
+    public static final int CODE_UNDEFINED = 7;
+    public static final int CODE_INT16_S = 8;
+    public static final int CODE_INT32_S = 9;
+    public static final int CODE_RATIONAL_S = 10;
+    public static final int CODE_SINGLE = 11;
+    public static final int CODE_DOUBLE = 12;
+
+    @NotNull public static final TiffDataFormat INT8_U = new TiffDataFormat("BYTE", CODE_INT8_U, 1);
+    @NotNull public static final TiffDataFormat STRING = new TiffDataFormat("STRING", CODE_STRING, 1);
+    @NotNull public static final TiffDataFormat INT16_U = new TiffDataFormat("USHORT", CODE_INT16_U, 2);
+    @NotNull public static final TiffDataFormat INT32_U = new TiffDataFormat("ULONG", CODE_INT32_U, 4);
+    @NotNull public static final TiffDataFormat RATIONAL_U = new TiffDataFormat("URATIONAL", CODE_RATIONAL_U, 8);
+    @NotNull public static final TiffDataFormat INT8_S = new TiffDataFormat("SBYTE", CODE_INT8_S, 1);
+    @NotNull public static final TiffDataFormat UNDEFINED = new TiffDataFormat("UNDEFINED", CODE_UNDEFINED, 1);
+    @NotNull public static final TiffDataFormat INT16_S = new TiffDataFormat("SSHORT", CODE_INT16_S, 2);
+    @NotNull public static final TiffDataFormat INT32_S = new TiffDataFormat("SLONG", CODE_INT32_S, 4);
+    @NotNull public static final TiffDataFormat RATIONAL_S = new TiffDataFormat("SRATIONAL", CODE_RATIONAL_S, 8);
+    @NotNull public static final TiffDataFormat SINGLE = new TiffDataFormat("SINGLE", CODE_SINGLE, 4);
+    @NotNull public static final TiffDataFormat DOUBLE = new TiffDataFormat("DOUBLE", CODE_DOUBLE, 8);
+
+    @NotNull
+    private final String _name;
+    private final int _tiffFormatCode;
+    private final int _componentSizeBytes;
+
+    @Nullable
+    public static TiffDataFormat fromTiffFormatCode(int tiffFormatCode)
+    {
+        switch (tiffFormatCode) {
+            case 1: return INT8_U;
+            case 2: return STRING;
+            case 3: return INT16_U;
+            case 4: return INT32_U;
+            case 5: return RATIONAL_U;
+            case 6: return INT8_S;
+            case 7: return UNDEFINED;
+            case 8: return INT16_S;
+            case 9: return INT32_S;
+            case 10: return RATIONAL_S;
+            case 11: return SINGLE;
+            case 12: return DOUBLE;
+        }
+        return null;
+    }
+
+    private TiffDataFormat(@NotNull String name, int tiffFormatCode, int componentSizeBytes)
+    {
+        _name = name;
+        _tiffFormatCode = tiffFormatCode;
+        _componentSizeBytes = componentSizeBytes;
+    }
+
+    public int getComponentSizeBytes()
+    {
+        return _componentSizeBytes;
+    }
+
+    public int getTiffFormatCode()
+    {
+        return _tiffFormatCode;
+    }
+
+    @Override
+    @NotNull
+    public String toString()
+    {
+        return _name;
+    }
+}
diff --git a/src/com/drew/imaging/tiff/TiffHandler.java b/src/com/drew/imaging/tiff/TiffHandler.java
new file mode 100644
index 0000000..1e606a6
--- /dev/null
+++ b/src/com/drew/imaging/tiff/TiffHandler.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.tiff;
+
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Interface of an class capable of handling events raised during the reading of a TIFF file
+ * via {@link TiffReader}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public interface TiffHandler
+{
+    /**
+     * Receives the 2-byte marker found in the TIFF header.
+     * <p>
+     * Implementations are not obligated to use this information for any purpose, though it may be useful for
+     * validation or perhaps differentiating the type of mapping to use for observed tags and IFDs.
+     *
+     * @param marker the 2-byte value found at position 2 of the TIFF header
+     */
+    void setTiffMarker(int marker) throws TiffProcessingException;
+
+    boolean isTagIfdPointer(int tagType);
+    boolean hasFollowerIfd();
+
+    void endingIFD();
+
+    void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset);
+
+    boolean customProcessTag(int tagOffset,
+                             @NotNull Set<Integer> processedIfdOffsets,
+                             int tiffHeaderOffset,
+                             @NotNull RandomAccessReader reader,
+                             int tagId,
+                             int byteCount) throws IOException;
+
+    void warn(@NotNull String message);
+    void error(@NotNull String message);
+
+    void setByteArray(int tagId, @NotNull byte[] bytes);
+    void setString(int tagId, @NotNull String string);
+    void setRational(int tagId, @NotNull Rational rational);
+    void setRationalArray(int tagId, @NotNull Rational[] array);
+    void setFloat(int tagId, float float32);
+    void setFloatArray(int tagId, @NotNull float[] array);
+    void setDouble(int tagId, double double64);
+    void setDoubleArray(int tagId, @NotNull double[] array);
+    void setInt8s(int tagId, byte int8s);
+    void setInt8sArray(int tagId, @NotNull byte[] array);
+    void setInt8u(int tagId, short int8u);
+    void setInt8uArray(int tagId, @NotNull short[] array);
+    void setInt16s(int tagId, int int16s);
+    void setInt16sArray(int tagId, @NotNull short[] array);
+    void setInt16u(int tagId, int int16u);
+    void setInt16uArray(int tagId, @NotNull int[] array);
+    void setInt32s(int tagId, int int32s);
+    void setInt32sArray(int tagId, @NotNull int[] array);
+    void setInt32u(int tagId, long int32u);
+    void setInt32uArray(int tagId, @NotNull long[] array);
+}
diff --git a/src/com/drew/imaging/tiff/TiffProcessingException.java b/src/com/drew/imaging/tiff/TiffProcessingException.java
new file mode 100644
index 0000000..5d90c9c
--- /dev/null
+++ b/src/com/drew/imaging/tiff/TiffProcessingException.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.imaging.tiff;
+
+import com.drew.imaging.ImageProcessingException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An exception class thrown upon unexpected and fatal conditions while processing a TIFF file.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Darren Salomons
+ */
+public class TiffProcessingException extends ImageProcessingException
+{
+    private static final long serialVersionUID = -1658134119488001891L;
+
+    public TiffProcessingException(@Nullable String message)
+    {
+        super(message);
+    }
+
+    public TiffProcessingException(@Nullable String message, @Nullable Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public TiffProcessingException(@Nullable Throwable cause)
+    {
+        super(cause);
+    }
+}
diff --git a/src/com/drew/imaging/tiff/TiffReader.java b/src/com/drew/imaging/tiff/TiffReader.java
new file mode 100644
index 0000000..497a64e
--- /dev/null
+++ b/src/com/drew/imaging/tiff/TiffReader.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.tiff;
+
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Processes TIFF-formatted data, calling into client code via that {@link TiffHandler} interface.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class TiffReader
+{
+    /**
+     * Processes a TIFF data sequence.
+     *
+     * @param reader the {@link RandomAccessReader} from which the data should be read
+     * @param handler the {@link TiffHandler} that will coordinate processing and accept read values
+     * @param tiffHeaderOffset the offset within <code>reader</code> at which the TIFF header starts
+     * @throws TiffProcessingException if an error occurred during the processing of TIFF data that could not be
+     *                                 ignored or recovered from
+     * @throws IOException an error occurred while accessing the required data
+     */
+    public void processTiff(@NotNull final RandomAccessReader reader,
+                            @NotNull final TiffHandler handler,
+                            final int tiffHeaderOffset) throws TiffProcessingException, IOException
+    {
+        // This must be either "MM" or "II".
+        short byteOrderIdentifier = reader.getInt16(tiffHeaderOffset);
+
+        if (byteOrderIdentifier == 0x4d4d) { // "MM"
+            reader.setMotorolaByteOrder(true);
+        } else if (byteOrderIdentifier == 0x4949) { // "II"
+            reader.setMotorolaByteOrder(false);
+        } else {
+            throw new TiffProcessingException("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);
+        }
+
+        // Check the next two values for correctness.
+        final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset);
+        handler.setTiffMarker(tiffMarker);
+
+        int firstIfdOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;
+
+        // David Ekholm sent a digital camera image that has this problem
+        // TODO getLength should be avoided as it causes RandomAccessStreamReader to read to the end of the stream
+        if (firstIfdOffset >= reader.getLength() - 1) {
+            handler.warn("First IFD offset is beyond the end of the TIFF data segment -- trying default offset");
+            // First directory normally starts immediately after the offset bytes, so try that
+            firstIfdOffset = tiffHeaderOffset + 2 + 2 + 4;
+        }
+
+        Set<Integer> processedIfdOffsets = new HashSet<Integer>();
+        processIfd(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset);
+
+        handler.completed(reader, tiffHeaderOffset);
+    }
+
+    /**
+     * Processes a TIFF IFD.
+     *
+     * IFD Header:
+     * <ul>
+     *     <li><b>2 bytes</b> number of tags</li>
+     * </ul>
+     * Tag structure:
+     * <ul>
+     *     <li><b>2 bytes</b> tag type</li>
+     *     <li><b>2 bytes</b> format code (values 1 to 12, inclusive)</li>
+     *     <li><b>4 bytes</b> component count</li>
+     *     <li><b>4 bytes</b> inline value, or offset pointer if too large to fit in four bytes</li>
+     * </ul>
+     *
+     *
+     * @param handler the {@link com.drew.imaging.tiff.TiffHandler} that will coordinate processing and accept read values
+     * @param reader the {@link com.drew.lang.RandomAccessReader} from which the data should be read
+     * @param processedIfdOffsets the set of visited IFD offsets, to avoid revisiting the same IFD in an endless loop
+     * @param ifdOffset the offset within <code>reader</code> at which the IFD data starts
+     * @param tiffHeaderOffset the offset within <code>reader</code> at which the TIFF header starts
+     * @throws IOException an error occurred while accessing the required data
+     */
+    public static void processIfd(@NotNull final TiffHandler handler,
+                                  @NotNull final RandomAccessReader reader,
+                                  @NotNull final Set<Integer> processedIfdOffsets,
+                                  final int ifdOffset,
+                                  final int tiffHeaderOffset) throws IOException
+    {
+        try {
+            // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
+            if (processedIfdOffsets.contains(Integer.valueOf(ifdOffset))) {
+                return;
+            }
+
+            // remember that we've visited this directory so that we don't visit it again later
+            processedIfdOffsets.add(ifdOffset);
+
+            if (ifdOffset >= reader.getLength() || ifdOffset < 0) {
+                handler.error("Ignored IFD marked to start outside data segment");
+                return;
+            }
+
+            // First two bytes in the IFD are the number of tags in this directory
+            int dirTagCount = reader.getUInt16(ifdOffset);
+
+            int dirLength = (2 + (12 * dirTagCount) + 4);
+            if (dirLength + ifdOffset > reader.getLength()) {
+                handler.error("Illegally sized IFD");
+                return;
+            }
+
+            //
+            // Handle each tag in this directory
+            //
+            int invalidTiffFormatCodeCount = 0;
+            for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {
+                final int tagOffset = calculateTagOffset(ifdOffset, tagNumber);
+
+                // 2 bytes for the tag id
+                final int tagId = reader.getUInt16(tagOffset);
+
+                // 2 bytes for the format code
+                final int formatCode = reader.getUInt16(tagOffset + 2);
+                final TiffDataFormat format = TiffDataFormat.fromTiffFormatCode(formatCode);
+
+                if (format == null) {
+                    // This error suggests that we are processing at an incorrect index and will generate
+                    // rubbish until we go out of bounds (which may be a while).  Exit now.
+                    handler.error("Invalid TIFF tag format code: " + formatCode);
+                    // TODO specify threshold as a parameter, or provide some other external control over this behaviour
+                    if (++invalidTiffFormatCodeCount > 5) {
+                        handler.error("Stopping processing as too many errors seen in TIFF IFD");
+                        return;
+                    }
+                    continue;
+                }
+
+                // 4 bytes dictate the number of components in this tag's data
+                final int componentCount = reader.getInt32(tagOffset + 4);
+                if (componentCount < 0) {
+                    handler.error("Negative TIFF tag component count");
+                    continue;
+                }
+
+                final int byteCount = componentCount * format.getComponentSizeBytes();
+
+                final int tagValueOffset;
+                if (byteCount > 4) {
+                    // If it's bigger than 4 bytes, the dir entry contains an offset.
+                    final int offsetVal = reader.getInt32(tagOffset + 8);
+                    if (offsetVal + byteCount > reader.getLength()) {
+                        // Bogus pointer offset and / or byteCount value
+                        handler.error("Illegal TIFF tag pointer offset");
+                        continue;
+                    }
+                    tagValueOffset = tiffHeaderOffset + offsetVal;
+                } else {
+                    // 4 bytes or less and value is in the dir entry itself.
+                    tagValueOffset = tagOffset + 8;
+                }
+
+                if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) {
+                    handler.error("Illegal TIFF tag pointer offset");
+                    continue;
+                }
+
+                // Check that this tag isn't going to allocate outside the bounds of the data array.
+                // This addresses an uncommon OutOfMemoryError.
+                if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) {
+                    handler.error("Illegal number of bytes for TIFF tag data: " + byteCount);
+                    continue;
+                }
+
+                //
+                // Special handling for tags that point to other IFDs
+                //
+                if (byteCount == 4 && handler.isTagIfdPointer(tagId)) {
+                    final int subDirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
+                    processIfd(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset);
+                } else {
+                    if (!handler.customProcessTag(tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, byteCount)) {
+                        processTag(handler, tagId, tagValueOffset, componentCount, formatCode, reader);
+                    }
+                }
+            }
+
+            // at the end of each IFD is an optional link to the next IFD
+            final int finalTagOffset = calculateTagOffset(ifdOffset, dirTagCount);
+            int nextIfdOffset = reader.getInt32(finalTagOffset);
+            if (nextIfdOffset != 0) {
+                nextIfdOffset += tiffHeaderOffset;
+                if (nextIfdOffset >= reader.getLength()) {
+                    // Last 4 bytes of IFD reference another IFD with an address that is out of bounds
+                    // Note this could have been caused by jhead 1.3 cropping too much
+                    return;
+                } else if (nextIfdOffset < ifdOffset) {
+                    // TODO is this a valid restriction?
+                    // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory
+                    return;
+                }
+
+                if (handler.hasFollowerIfd()) {
+                    processIfd(handler, reader, processedIfdOffsets, nextIfdOffset, tiffHeaderOffset);
+                }
+            }
+        } finally {
+            handler.endingIFD();
+        }
+    }
+
+    private static void processTag(@NotNull final TiffHandler handler,
+                                   final int tagId,
+                                   final int tagValueOffset,
+                                   final int componentCount,
+                                   final int formatCode,
+                                   @NotNull final RandomAccessReader reader) throws IOException
+    {
+        switch (formatCode) {
+            case TiffDataFormat.CODE_UNDEFINED:
+                // this includes exif user comments
+                handler.setByteArray(tagId, reader.getBytes(tagValueOffset, componentCount));
+                break;
+            case TiffDataFormat.CODE_STRING:
+                handler.setString(tagId, reader.getNullTerminatedString(tagValueOffset, componentCount));
+                break;
+            case TiffDataFormat.CODE_RATIONAL_S:
+                if (componentCount == 1) {
+                    handler.setRational(tagId, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));
+                } else if (componentCount > 1) {
+                    Rational[] array = new Rational[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));
+                    handler.setRationalArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_RATIONAL_U:
+                if (componentCount == 1) {
+                    handler.setRational(tagId, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));
+                } else if (componentCount > 1) {
+                    Rational[] array = new Rational[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));
+                    handler.setRationalArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_SINGLE:
+                if (componentCount == 1) {
+                    handler.setFloat(tagId, reader.getFloat32(tagValueOffset));
+                } else {
+                    float[] array = new float[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getFloat32(tagValueOffset + (i * 4));
+                    handler.setFloatArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_DOUBLE:
+                if (componentCount == 1) {
+                    handler.setDouble(tagId, reader.getDouble64(tagValueOffset));
+                } else {
+                    double[] array = new double[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getDouble64(tagValueOffset + (i * 4));
+                    handler.setDoubleArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT8_S:
+                if (componentCount == 1) {
+                    handler.setInt8s(tagId, reader.getInt8(tagValueOffset));
+                } else {
+                    byte[] array = new byte[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getInt8(tagValueOffset + i);
+                    handler.setInt8sArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT8_U:
+                if (componentCount == 1) {
+                    handler.setInt8u(tagId, reader.getUInt8(tagValueOffset));
+                } else {
+                    short[] array = new short[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getUInt8(tagValueOffset + i);
+                    handler.setInt8uArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT16_S:
+                if (componentCount == 1) {
+                    handler.setInt16s(tagId, (int)reader.getInt16(tagValueOffset));
+                } else {
+                    short[] array = new short[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getInt16(tagValueOffset + (i * 2));
+                    handler.setInt16sArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT16_U:
+                if (componentCount == 1) {
+                    handler.setInt16u(tagId, reader.getUInt16(tagValueOffset));
+                } else {
+                    int[] array = new int[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getUInt16(tagValueOffset + (i * 2));
+                    handler.setInt16uArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT32_S:
+                // NOTE 'long' in this case means 32 bit, not 64
+                if (componentCount == 1) {
+                    handler.setInt32s(tagId, reader.getInt32(tagValueOffset));
+                } else {
+                    int[] array = new int[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getInt32(tagValueOffset + (i * 4));
+                    handler.setInt32sArray(tagId, array);
+                }
+                break;
+            case TiffDataFormat.CODE_INT32_U:
+                // NOTE 'long' in this case means 32 bit, not 64
+                if (componentCount == 1) {
+                    handler.setInt32u(tagId, reader.getUInt32(tagValueOffset));
+                } else {
+                    long[] array = new long[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        array[i] = reader.getUInt32(tagValueOffset + (i * 4));
+                    handler.setInt32uArray(tagId, array);
+                }
+                break;
+            default:
+                handler.error(String.format("Unknown format code %d for tag %d", formatCode, tagId));
+        }
+    }
+
+    /**
+     * Determine the offset of a given tag within the specified IFD.
+     *
+     * @param ifdStartOffset the offset at which the IFD starts
+     * @param entryNumber    the zero-based entry number
+     */
+    private static int calculateTagOffset(int ifdStartOffset, int entryNumber)
+    {
+        // Add 2 bytes for the tag count.
+        // Each entry is 12 bytes.
+        return ifdStartOffset + 2 + (12 * entryNumber);
+    }
+}
diff --git a/src/com/drew/imaging/tiff/package.html b/src/com/drew/imaging/tiff/package.html
new file mode 100644
index 0000000..ce5b2e6
--- /dev/null
+++ b/src/com/drew/imaging/tiff/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for working with TIFF format files.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/lang/BufferBoundsException.java b/src/com/drew/lang/BufferBoundsException.java
new file mode 100755
index 0000000..b7ecab4
--- /dev/null
+++ b/src/com/drew/lang/BufferBoundsException.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import java.io.IOException;
+
+/**
+ * A checked replacement for {@link IndexOutOfBoundsException}.  Used by {@link RandomAccessReader}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class BufferBoundsException extends IOException
+{
+    private static final long serialVersionUID = 2911102837808946396L;
+
+    public BufferBoundsException(int index, int bytesRequested, long bufferLength)
+    {
+        super(getMessage(index, bytesRequested, bufferLength));
+    }
+
+    public BufferBoundsException(final String message)
+    {
+        super(message);
+    }
+
+    private static String getMessage(int index, int bytesRequested, long bufferLength)
+    {
+        if (index < 0)
+            return String.format("Attempt to read from buffer using a negative index (%d)", index);
+
+        if (bytesRequested < 0)
+            return String.format("Number of requested bytes cannot be negative (%d)", bytesRequested);
+
+        if ((long)index + (long)bytesRequested - 1L > (long)Integer.MAX_VALUE)
+            return String.format("Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: %d, requested count: %d)", index, bytesRequested);
+
+        return String.format("Attempt to read from beyond end of underlying data source (requested index: %d, requested count: %d, max index: %d)",
+                index, bytesRequested, bufferLength - 1);
+    }
+}
diff --git a/src/com/drew/lang/ByteArrayReader.java b/src/com/drew/lang/ByteArrayReader.java
new file mode 100755
index 0000000..397d8ae
--- /dev/null
+++ b/src/com/drew/lang/ByteArrayReader.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+
+/**
+ * Provides methods to read specific values from a byte array, with a consistent, checked exception structure for
+ * issues.
+ * <p>
+ * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
+ * <code>setMotorolaByteOrder(boolean)</code>.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * */
+public class ByteArrayReader extends RandomAccessReader
+{
+    @NotNull
+    private final byte[] _buffer;
+
+    @SuppressWarnings({ "ConstantConditions" })
+    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
+    public ByteArrayReader(@NotNull byte[] buffer)
+    {
+        if (buffer == null)
+            throw new NullPointerException();
+
+        _buffer = buffer;
+    }
+
+    @Override
+    public long getLength()
+    {
+        return _buffer.length;
+    }
+
+    @Override
+    protected byte getByte(int index) throws IOException
+    {
+        return _buffer[index];
+    }
+
+    @Override
+    protected void validateIndex(int index, int bytesRequested) throws IOException
+    {
+        if (!isValidIndex(index, bytesRequested))
+            throw new BufferBoundsException(index, bytesRequested, _buffer.length);
+    }
+
+    @Override
+    protected boolean isValidIndex(int index, int bytesRequested) throws IOException
+    {
+        return bytesRequested >= 0
+            && index >= 0
+            && (long)index + (long)bytesRequested - 1L < (long)_buffer.length;
+    }
+
+    @Override
+    @NotNull
+    public byte[] getBytes(int index, int count) throws IOException
+    {
+        validateIndex(index, count);
+
+        byte[] bytes = new byte[count];
+        System.arraycopy(_buffer, index, bytes, 0, count);
+        return bytes;
+    }
+}
diff --git a/src/com/drew/lang/CompoundException.java b/src/com/drew/lang/CompoundException.java
new file mode 100644
index 0000000..a7ed181
--- /dev/null
+++ b/src/com/drew/lang/CompoundException.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * Represents a compound exception, as modelled in JDK 1.4, but
+ * unavailable in previous versions.  This class allows support
+ * of these previous JDK versions.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CompoundException extends Exception
+{
+    private static final long serialVersionUID = -9207883813472069925L;
+
+    @Nullable
+    private final Throwable _innerException;
+
+    public CompoundException(@Nullable String msg)
+    {
+        this(msg, null);
+    }
+
+    public CompoundException(@Nullable Throwable exception)
+    {
+        this(null, exception);
+    }
+
+    public CompoundException(@Nullable String msg, @Nullable Throwable innerException)
+    {
+        super(msg);
+        _innerException = innerException;
+    }
+
+    @Nullable
+    public Throwable getInnerException()
+    {
+        return _innerException;
+    }
+
+    @Override
+    @NotNull
+    public String toString()
+    {
+        StringBuilder string = new StringBuilder();
+        string.append(super.toString());
+        if (_innerException != null) {
+            string.append("\n");
+            string.append("--- inner exception ---");
+            string.append("\n");
+            string.append(_innerException.toString());
+        }
+        return string.toString();
+    }
+
+    @Override
+    public void printStackTrace(@NotNull PrintStream s)
+    {
+        super.printStackTrace(s);
+        if (_innerException != null) {
+            s.println("--- inner exception ---");
+            _innerException.printStackTrace(s);
+        }
+    }
+
+    @Override
+    public void printStackTrace(@NotNull PrintWriter s)
+    {
+        super.printStackTrace(s);
+        if (_innerException != null) {
+            s.println("--- inner exception ---");
+            _innerException.printStackTrace(s);
+        }
+    }
+
+    @Override
+    public void printStackTrace()
+    {
+        super.printStackTrace();
+        if (_innerException != null) {
+            System.err.println("--- inner exception ---");
+            _innerException.printStackTrace();
+        }
+    }
+}
diff --git a/src/com/drew/lang/GeoLocation.java b/src/com/drew/lang/GeoLocation.java
new file mode 100755
index 0000000..a006a59
--- /dev/null
+++ b/src/com/drew/lang/GeoLocation.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.text.DecimalFormat;
+
+/**
+ * Represents a latitude and longitude pair, giving a position on earth in spherical coordinates.
+ * <p>
+ * Values of latitude and longitude are given in degrees.
+ * <p>
+ * This type is immutable.
+ */
+public final class GeoLocation
+{
+    private final double _latitude;
+    private final double _longitude;
+
+    /**
+     * Instantiates a new instance of {@link GeoLocation}.
+     *
+     * @param latitude the latitude, in degrees
+     * @param longitude the longitude, in degrees
+     */
+    public GeoLocation(double latitude, double longitude)
+    {
+        _latitude = latitude;
+        _longitude = longitude;
+    }
+
+    /**
+     * @return the latitudinal angle of this location, in degrees.
+     */
+    public double getLatitude()
+    {
+        return _latitude;
+    }
+
+    /**
+     * @return the longitudinal angle of this location, in degrees.
+     */
+    public double getLongitude()
+    {
+        return _longitude;
+    }
+
+    /**
+     * @return true, if both latitude and longitude are equal to zero
+     */
+    public boolean isZero()
+    {
+        return _latitude == 0 && _longitude == 0;
+    }
+
+    /**
+     * Converts a decimal degree angle into its corresponding DMS (degrees-minutes-seconds) representation as a string,
+     * of format: {@code -1° 23' 4.56"}
+     */
+    @NotNull
+    public static String decimalToDegreesMinutesSecondsString(double decimal)
+    {
+        double[] dms = decimalToDegreesMinutesSeconds(decimal);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
+    }
+
+    /**
+     * Converts a decimal degree angle into its corresponding DMS (degrees-minutes-seconds) component values, as
+     * a double array.
+     */
+    @NotNull
+    public static double[] decimalToDegreesMinutesSeconds(double decimal)
+    {
+        int d = (int)decimal;
+        double m = Math.abs((decimal % 1) * 60);
+        double s = (m % 1) * 60;
+        return new double[] { d, (int)m, s};
+    }
+
+    /**
+     * Converts DMS (degrees-minutes-seconds) rational values, as given in {@link com.drew.metadata.exif.GpsDirectory},
+     * into a single value in degrees, as a double.
+     */
+    @Nullable
+    public static Double degreesMinutesSecondsToDecimal(@NotNull final Rational degs, @NotNull final Rational mins, @NotNull final Rational secs, final boolean isNegative)
+    {
+        double decimal = Math.abs(degs.doubleValue())
+                + mins.doubleValue() / 60.0d
+                + secs.doubleValue() / 3600.0d;
+
+        if (Double.isNaN(decimal))
+            return null;
+
+        if (isNegative)
+            decimal *= -1;
+
+        return decimal;
+    }
+
+    @Override
+    public boolean equals(final Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        GeoLocation that = (GeoLocation) o;
+        if (Double.compare(that._latitude, _latitude) != 0) return false;
+        if (Double.compare(that._longitude, _longitude) != 0) return false;
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result;
+        long temp;
+        temp = _latitude != +0.0d ? Double.doubleToLongBits(_latitude) : 0L;
+        result = (int) (temp ^ (temp >>> 32));
+        temp = _longitude != +0.0d ? Double.doubleToLongBits(_longitude) : 0L;
+        result = 31 * result + (int) (temp ^ (temp >>> 32));
+        return result;
+    }
+
+    /**
+     * @return a string representation of this location, of format: {@code 1.23, 4.56}
+     */
+    @Override
+    @NotNull
+    public String toString()
+    {
+        return _latitude + ", " + _longitude;
+    }
+
+    /**
+     * @return a string representation of this location, of format: {@code -1° 23' 4.56", 54° 32' 1.92"}
+     */
+    @NotNull
+    public String toDMSString()
+    {
+        return decimalToDegreesMinutesSecondsString(_latitude) + ", " + decimalToDegreesMinutesSecondsString(_longitude);
+    }
+}
diff --git a/src/com/drew/lang/NullOutputStream.java b/src/com/drew/lang/NullOutputStream.java
new file mode 100644
index 0000000..3e9b583
--- /dev/null
+++ b/src/com/drew/lang/NullOutputStream.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.lang;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An implementation of OutputSteam that ignores write requests by doing nothing.  This class may be useful in tests.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NullOutputStream extends OutputStream
+{
+    public NullOutputStream()
+    {
+        super();
+    }
+
+    @Override
+    public void write(int b) throws IOException
+    {
+        // do nothing
+    }
+}
diff --git a/src/com/drew/lang/RandomAccessReader.java b/src/com/drew/lang/RandomAccessReader.java
new file mode 100644
index 0000000..e93fc2d
--- /dev/null
+++ b/src/com/drew/lang/RandomAccessReader.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Base class for random access data reading operations of common data types.
+ * <p>
+ * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
+ * {@link com.drew.lang.RandomAccessReader#setMotorolaByteOrder(boolean)}.
+ * <p>
+ * Concrete implementations include:
+ * <ul>
+ *     <li>{@link ByteArrayReader}</li>
+ *     <li>{@link RandomAccessStreamReader}</li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class RandomAccessReader
+{
+    private boolean _isMotorolaByteOrder = true;
+
+    /**
+     * Gets the byte value at the specified byte <code>index</code>.
+     * <p>
+     * Implementations should not perform any bounds checking in this method. That should be performed
+     * in <code>validateIndex</code> and <code>isValidIndex</code>.
+     *
+     * @param index The index from which to read the byte
+     * @return The read byte value
+     * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative
+     * @throws BufferBoundsException if the requested byte is beyond the end of the underlying data source
+     * @throws IOException if the byte is unable to be read
+     */
+    protected abstract byte getByte(int index) throws IOException;
+
+    /**
+     * Returns the required number of bytes from the specified index from the underlying source.
+     *
+     * @param index The index from which the bytes begins in the underlying source
+     * @param count The number of bytes to be returned
+     * @return The requested bytes
+     * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative
+     * @throws BufferBoundsException if the requested bytes extend beyond the end of the underlying data source
+     * @throws IOException if the byte is unable to be read
+     */
+    @NotNull
+    public abstract byte[] getBytes(int index, int count) throws IOException;
+
+    /**
+     * Ensures that the buffered bytes extend to cover the specified index. If not, an attempt is made
+     * to read to that point.
+     * <p>
+     * If the stream ends before the point is reached, a {@link BufferBoundsException} is raised.
+     *
+     * @param index the index from which the required bytes start
+     * @param bytesRequested the number of bytes which are required
+     * @throws IOException if the stream ends before the required number of bytes are acquired
+     */
+    protected abstract void validateIndex(int index, int bytesRequested) throws IOException;
+
+    protected abstract boolean isValidIndex(int index, int bytesRequested) throws IOException;
+
+    /**
+     * Returns the length of the data source in bytes.
+     * <p>
+     * This is a simple operation for implementations (such as {@link RandomAccessFileReader} and
+     * {@link ByteArrayReader}) that have the entire data source available.
+     * <p>
+     * Users of this method must be aware that sequentially accessed implementations such as
+     * {@link RandomAccessStreamReader} will have to read and buffer the entire data source in
+     * order to determine the length.
+     *
+     * @return the length of the data source, in bytes.
+     */
+    public abstract long getLength() throws IOException;
+
+    /**
+     * Sets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     *
+     * @param motorolaByteOrder <code>true</code> for Motorola/big endian, <code>false</code> for Intel/little endian
+     */
+    public void setMotorolaByteOrder(boolean motorolaByteOrder)
+    {
+        _isMotorolaByteOrder = motorolaByteOrder;
+    }
+
+    /**
+     * Gets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     */
+    public boolean isMotorolaByteOrder()
+    {
+        return _isMotorolaByteOrder;
+    }
+
+    /**
+     * Gets whether a bit at a specific index is set or not.
+     *
+     * @param index the number of bits at which to test
+     * @return true if the bit is set, otherwise false
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public boolean getBit(int index) throws IOException
+    {
+        int byteIndex = index / 8;
+        int bitIndex = index % 8;
+
+        validateIndex(byteIndex, 1);
+
+        byte b = getByte(byteIndex);
+        return ((b >> bitIndex) & 1) == 1;
+    }
+
+    /**
+     * Returns an unsigned 8-bit int calculated from one byte of data at the specified index.
+     *
+     * @param index position within the data buffer to read byte
+     * @return the 8 bit int value, between 0 and 255
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public short getUInt8(int index) throws IOException
+    {
+        validateIndex(index, 1);
+
+        return (short) (getByte(index) & 0xFF);
+    }
+
+    /**
+     * Returns a signed 8-bit int calculated from one byte of data at the specified index.
+     *
+     * @param index position within the data buffer to read byte
+     * @return the 8 bit int value, between 0x00 and 0xFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public byte getInt8(int index) throws IOException
+    {
+        validateIndex(index, 1);
+
+        return getByte(index);
+    }
+
+    /**
+     * Returns an unsigned 16-bit int calculated from two bytes of data at the specified index.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public int getUInt16(int index) throws IOException
+    {
+        validateIndex(index, 2);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (getByte(index    ) << 8 & 0xFF00) |
+                   (getByte(index + 1)      & 0xFF);
+        } else {
+            // Intel ordering - LSB first
+            return (getByte(index + 1) << 8 & 0xFF00) |
+                   (getByte(index    )      & 0xFF);
+        }
+    }
+
+    /**
+     * Returns a signed 16-bit int calculated from two bytes of data at the specified index (MSB, LSB).
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public short getInt16(int index) throws IOException
+    {
+        validateIndex(index, 2);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (short) (((short)getByte(index    ) << 8 & (short)0xFF00) |
+                            ((short)getByte(index + 1)      & (short)0xFF));
+        } else {
+            // Intel ordering - LSB first
+            return (short) (((short)getByte(index + 1) << 8 & (short)0xFF00) |
+                            ((short)getByte(index    )      & (short)0xFF));
+        }
+    }
+
+    /**
+     * Get a 24-bit unsigned integer from the buffer, returning it as an int.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the unsigned 24-bit int value as a long, between 0x00000000 and 0x00FFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public int getInt24(int index) throws IOException
+    {
+        validateIndex(index, 3);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (((int)getByte(index    )) << 16 & 0xFF0000) |
+                   (((int)getByte(index + 1)) << 8  & 0xFF00) |
+                   (((int)getByte(index + 2))       & 0xFF);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (((int)getByte(index + 2)) << 16 & 0xFF0000) |
+                   (((int)getByte(index + 1)) << 8  & 0xFF00) |
+                   (((int)getByte(index    ))       & 0xFF);
+        }
+    }
+
+    /**
+     * Get a 32-bit unsigned integer from the buffer, returning it as a long.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public long getUInt32(int index) throws IOException
+    {
+        validateIndex(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (((long)getByte(index    )) << 24 & 0xFF000000L) |
+                   (((long)getByte(index + 1)) << 16 & 0xFF0000L) |
+                   (((long)getByte(index + 2)) << 8  & 0xFF00L) |
+                   (((long)getByte(index + 3))       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (((long)getByte(index + 3)) << 24 & 0xFF000000L) |
+                   (((long)getByte(index + 2)) << 16 & 0xFF0000L) |
+                   (((long)getByte(index + 1)) << 8  & 0xFF00L) |
+                   (((long)getByte(index    ))       & 0xFFL);
+        }
+    }
+
+    /**
+     * Returns a signed 32-bit integer from four bytes of data at the specified index the buffer.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public int getInt32(int index) throws IOException
+    {
+        validateIndex(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (getByte(index    ) << 24 & 0xFF000000) |
+                   (getByte(index + 1) << 16 & 0xFF0000) |
+                   (getByte(index + 2) << 8  & 0xFF00) |
+                   (getByte(index + 3)       & 0xFF);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (getByte(index + 3) << 24 & 0xFF000000) |
+                   (getByte(index + 2) << 16 & 0xFF0000) |
+                   (getByte(index + 1) << 8  & 0xFF00) |
+                   (getByte(index    )       & 0xFF);
+        }
+    }
+
+    /**
+     * Get a signed 64-bit integer from the buffer.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the 64 bit int value, between 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public long getInt64(int index) throws IOException
+    {
+        validateIndex(index, 8);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return ((long)getByte(index    ) << 56 & 0xFF00000000000000L) |
+                   ((long)getByte(index + 1) << 48 & 0xFF000000000000L) |
+                   ((long)getByte(index + 2) << 40 & 0xFF0000000000L) |
+                   ((long)getByte(index + 3) << 32 & 0xFF00000000L) |
+                   ((long)getByte(index + 4) << 24 & 0xFF000000L) |
+                   ((long)getByte(index + 5) << 16 & 0xFF0000L) |
+                   ((long)getByte(index + 6) << 8  & 0xFF00L) |
+                   ((long)getByte(index + 7)       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first
+            return ((long)getByte(index + 7) << 56 & 0xFF00000000000000L) |
+                   ((long)getByte(index + 6) << 48 & 0xFF000000000000L) |
+                   ((long)getByte(index + 5) << 40 & 0xFF0000000000L) |
+                   ((long)getByte(index + 4) << 32 & 0xFF00000000L) |
+                   ((long)getByte(index + 3) << 24 & 0xFF000000L) |
+                   ((long)getByte(index + 2) << 16 & 0xFF0000L) |
+                   ((long)getByte(index + 1) << 8  & 0xFF00L) |
+                   ((long)getByte(index    )       & 0xFFL);
+        }
+    }
+
+    /**
+     * Gets a s15.16 fixed point float from the buffer.
+     * <p>
+     * This particular fixed point encoding has one sign bit, 15 numerator bits and 16 denominator bits.
+     *
+     * @return the floating point value
+     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    public float getS15Fixed16(int index) throws IOException
+    {
+        validateIndex(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            float res = (getByte(index    ) & 0xFF) << 8 |
+                        (getByte(index + 1) & 0xFF);
+            int d =     (getByte(index + 2) & 0xFF) << 8 |
+                        (getByte(index + 3) & 0xFF);
+            return (float)(res + d/65536.0);
+        } else {
+            // this particular branch is untested
+            float res = (getByte(index + 3) & 0xFF) << 8 |
+                        (getByte(index + 2) & 0xFF);
+            int d =     (getByte(index + 1) & 0xFF) << 8 |
+                        (getByte(index    ) & 0xFF);
+            return (float)(res + d/65536.0);
+        }
+    }
+
+    public float getFloat32(int index) throws IOException
+    {
+        return Float.intBitsToFloat(getInt32(index));
+    }
+
+    public double getDouble64(int index) throws IOException
+    {
+        return Double.longBitsToDouble(getInt64(index));
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested) throws IOException
+    {
+        return new String(getBytes(index, bytesRequested));
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested, String charset) throws IOException
+    {
+        byte[] bytes = getBytes(index, bytesRequested);
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return new String(bytes);
+        }
+    }
+
+    /**
+     * Creates a String from the _data buffer starting at the specified index,
+     * and ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
+     *
+     * @param index          The index within the buffer at which to start reading the string.
+     * @param maxLengthBytes The maximum number of bytes to read.  If a zero-byte is not reached within this limit,
+     *                       reading will stop and the string will be truncated to this length.
+     * @return The read string.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public String getNullTerminatedString(int index, int maxLengthBytes) throws IOException
+    {
+        // NOTE currently only really suited to single-byte character strings
+
+        byte[] bytes = getBytes(index, maxLengthBytes);
+
+        // Count the number of non-null bytes
+        int length = 0;
+        while (length < bytes.length && bytes[length] != '\0')
+            length++;
+
+        return new String(bytes, 0, length);
+    }
+}
diff --git a/src/com/drew/lang/Rational.java b/src/com/drew/lang/Rational.java
new file mode 100644
index 0000000..5b80617
--- /dev/null
+++ b/src/com/drew/lang/Rational.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.Serializable;
+
+/**
+ * Immutable class for holding a rational number without loss of precision.  Provides
+ * a familiar representation via {@link Rational#toString} in form <code>numerator/denominator</code>.
+ *
+ * Note that any value with a numerator of zero will be treated as zero, even if the
+ * denominator is also zero.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class Rational extends java.lang.Number implements Serializable
+{
+    private static final long serialVersionUID = 510688928138848770L;
+
+    /** Holds the numerator. */
+    private final long _numerator;
+
+    /** Holds the denominator. */
+    private final long _denominator;
+
+    /**
+     * Creates a new instance of Rational.  Rational objects are immutable, so
+     * once you've set your numerator and denominator values here, you're stuck
+     * with them!
+     */
+    public Rational(long numerator, long denominator)
+    {
+        _numerator = numerator;
+        _denominator = denominator;
+    }
+
+    /**
+     * Returns the value of the specified number as a <code>double</code>.
+     * This may involve rounding.
+     *
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>double</code>.
+     */
+    @Override
+    public double doubleValue()
+    {
+        return _numerator == 0
+            ? 0.0
+            : (double) _numerator / (double) _denominator;
+    }
+
+    /**
+     * Returns the value of the specified number as a <code>float</code>.
+     * This may involve rounding.
+     *
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>float</code>.
+     */
+    @Override
+    public float floatValue()
+    {
+        return _numerator == 0
+            ? 0.0f
+            : (float) _numerator / (float) _denominator;
+    }
+
+    /**
+     * Returns the value of the specified number as a <code>byte</code>.
+     * This may involve rounding or truncation.  This implementation simply
+     * casts the result of {@link Rational#doubleValue} to <code>byte</code>.
+     *
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>byte</code>.
+     */
+    @Override
+    public final byte byteValue()
+    {
+        return (byte) doubleValue();
+    }
+
+    /**
+     * Returns the value of the specified number as an <code>int</code>.
+     * This may involve rounding or truncation.  This implementation simply
+     * casts the result of {@link Rational#doubleValue} to <code>int</code>.
+     *
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>int</code>.
+     */
+    @Override
+    public final int intValue()
+    {
+        return (int) doubleValue();
+    }
+
+    /**
+     * Returns the value of the specified number as a <code>long</code>.
+     * This may involve rounding or truncation.  This implementation simply
+     * casts the result of {@link Rational#doubleValue} to <code>long</code>.
+     *
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>long</code>.
+     */
+    @Override
+    public final long longValue()
+    {
+        return (long) doubleValue();
+    }
+
+    /**
+     * Returns the value of the specified number as a <code>short</code>.
+     * This may involve rounding or truncation.  This implementation simply
+     * casts the result of {@link Rational#doubleValue} to <code>short</code>.
+     *
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>short</code>.
+     */
+    @Override
+    public final short shortValue()
+    {
+        return (short) doubleValue();
+    }
+
+
+    /** Returns the denominator. */
+    public final long getDenominator()
+    {
+        return this._denominator;
+    }
+
+    /** Returns the numerator. */
+    public final long getNumerator()
+    {
+        return this._numerator;
+    }
+
+    /**
+     * Returns the reciprocal value of this object as a new Rational.
+     *
+     * @return the reciprocal in a new object
+     */
+    @NotNull
+    public Rational getReciprocal()
+    {
+        return new Rational(this._denominator, this._numerator);
+    }
+
+    /** Checks if this {@link Rational} number is an Integer, either positive or negative. */
+    public boolean isInteger()
+    {
+        return _denominator == 1 ||
+                (_denominator != 0 && (_numerator % _denominator == 0)) ||
+                (_denominator == 0 && _numerator == 0);
+    }
+
+    /**
+     * Returns a string representation of the object of form <code>numerator/denominator</code>.
+     *
+     * @return a string representation of the object.
+     */
+    @Override
+    @NotNull
+    public String toString()
+    {
+        return _numerator + "/" + _denominator;
+    }
+
+    /** Returns the simplest representation of this {@link Rational}'s value possible. */
+    @NotNull
+    public String toSimpleString(boolean allowDecimal)
+    {
+        if (_denominator == 0 && _numerator != 0) {
+            return toString();
+        } else if (isInteger()) {
+            return Integer.toString(intValue());
+        } else if (_numerator != 1 && _denominator % _numerator == 0) {
+            // common factor between denominator and numerator
+            long newDenominator = _denominator / _numerator;
+            return new Rational(1, newDenominator).toSimpleString(allowDecimal);
+        } else {
+            Rational simplifiedInstance = getSimplifiedInstance();
+            if (allowDecimal) {
+                String doubleString = Double.toString(simplifiedInstance.doubleValue());
+                if (doubleString.length() < 5) {
+                    return doubleString;
+                }
+            }
+            return simplifiedInstance.toString();
+        }
+    }
+
+    /**
+     * Decides whether a brute-force simplification calculation should be avoided
+     * by comparing the maximum number of possible calculations with some threshold.
+     *
+     * @return true if the simplification should be performed, otherwise false
+     */
+    private boolean tooComplexForSimplification()
+    {
+        double maxPossibleCalculations = (((double) (Math.min(_denominator, _numerator) - 1) / 5d) + 2);
+        final int maxSimplificationCalculations = 1000;
+        return maxPossibleCalculations > maxSimplificationCalculations;
+    }
+
+    /**
+     * Compares two {@link Rational} instances, returning true if they are mathematically
+     * equivalent.
+     *
+     * @param obj the {@link Rational} to compare this instance to.
+     * @return true if instances are mathematically equivalent, otherwise false.  Will also
+     *         return false if <code>obj</code> is not an instance of {@link Rational}.
+     */
+    @Override
+    public boolean equals(@Nullable Object obj)
+    {
+        if (obj==null || !(obj instanceof Rational))
+            return false;
+        Rational that = (Rational) obj;
+        return this.doubleValue() == that.doubleValue();
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return (23 * (int)_denominator) + (int)_numerator;
+    }
+
+    /**
+     * <p>
+     * Simplifies the {@link Rational} number.</p>
+     * <p>
+     * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p>
+     * <p>
+     * To reduce a rational, need to see if both numerator and denominator are divisible
+     * by a common factor.  Using the prime number series in ascending order guarantees
+     * the minimum number of checks required.</p>
+     * <p>
+     * However, generating the prime number series seems to be a hefty task.  Perhaps
+     * it's simpler to check if both d & n are divisible by all numbers from 2 {@literal ->}
+     * (Math.min(denominator, numerator) / 2).  In doing this, one can check for 2
+     * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5.
+     * This leaves four numbers from every ten to check.</p>
+     * <p>
+     * Therefore, the max number of pairs of modulus divisions required will be:</p>
+     * <pre><code>
+     *    4   Math.min(denominator, numerator) - 1
+     *   -- * ------------------------------------ + 2
+     *   10                    2
+     *
+     *   Math.min(denominator, numerator) - 1
+     * = ------------------------------------ + 2
+     *                  5
+     * </code></pre>
+     *
+     * @return a simplified instance, or if the Rational could not be simplified,
+     *         returns itself (unchanged)
+     */
+    @NotNull
+    public Rational getSimplifiedInstance()
+    {
+        if (tooComplexForSimplification()) {
+            return this;
+        }
+        for (int factor = 2; factor <= Math.min(_denominator, _numerator); factor++) {
+            if ((factor % 2 == 0 && factor > 2) || (factor % 5 == 0 && factor > 5)) {
+                continue;
+            }
+            if (_denominator % factor == 0 && _numerator % factor == 0) {
+                // found a common factor
+                return new Rational(_numerator / factor, _denominator / factor);
+            }
+        }
+        return this;
+    }
+}
diff --git a/src/com/drew/lang/SequentialByteArrayReader.java b/src/com/drew/lang/SequentialByteArrayReader.java
new file mode 100644
index 0000000..1173f3c
--- /dev/null
+++ b/src/com/drew/lang/SequentialByteArrayReader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SequentialByteArrayReader extends SequentialReader
+{
+    @NotNull
+    private final byte[] _bytes;
+    private int _index;
+
+    public SequentialByteArrayReader(@NotNull byte[] bytes)
+    {
+        this(bytes, 0);
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    public SequentialByteArrayReader(@NotNull byte[] bytes, int baseIndex)
+    {
+        if (bytes == null)
+            throw new NullPointerException();
+
+        _bytes = bytes;
+        _index = baseIndex;
+    }
+
+    @Override
+    protected byte getByte() throws IOException
+    {
+        if (_index >= _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+        return _bytes[_index++];
+    }
+
+    @NotNull
+    @Override
+    public byte[] getBytes(int count) throws IOException
+    {
+        if (_index + count > _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+
+        byte[] bytes = new byte[count];
+        System.arraycopy(_bytes, _index, bytes, 0, count);
+        _index += count;
+
+        return bytes;
+    }
+
+    @Override
+    public void skip(long n) throws IOException
+    {
+        if (n < 0) {
+            throw new IllegalArgumentException("n must be zero or greater.");
+        }
+
+        if (_index + n > _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+
+        _index += n;
+    }
+
+    @Override
+    public boolean trySkip(long n) throws IOException
+    {
+        if (n < 0) {
+            throw new IllegalArgumentException("n must be zero or greater.");
+        }
+
+        _index += n;
+
+        if (_index > _bytes.length) {
+            _index = _bytes.length;
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/src/com/drew/lang/SequentialReader.java b/src/com/drew/lang/SequentialReader.java
new file mode 100644
index 0000000..a1bcdb5
--- /dev/null
+++ b/src/com/drew/lang/SequentialReader.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class SequentialReader
+{
+    // TODO review whether the masks are needed (in both this and RandomAccessReader)
+
+    private boolean _isMotorolaByteOrder = true;
+
+    /**
+     * Gets the next byte in the sequence.
+     *
+     * @return The read byte value
+     */
+    protected abstract byte getByte() throws IOException;
+
+    /**
+     * Returns the required number of bytes from the sequence.
+     *
+     * @param count The number of bytes to be returned
+     * @return The requested bytes
+     */
+    @NotNull
+    public abstract byte[] getBytes(int count) throws IOException;
+
+    /**
+     * Skips forward in the sequence. If the sequence ends, an {@link EOFException} is thrown.
+     *
+     * @param n the number of byte to skip. Must be zero or greater.
+     * @throws EOFException the end of the sequence is reached.
+     * @throws IOException an error occurred reading from the underlying source.
+     */
+    public abstract void skip(long n) throws IOException;
+
+    /**
+     * Skips forward in the sequence, returning a boolean indicating whether the skip succeeded, or whether the sequence ended.
+     *
+     * @param n the number of byte to skip. Must be zero or greater.
+     * @return a boolean indicating whether the skip succeeded, or whether the sequence ended.
+     * @throws IOException an error occurred reading from the underlying source.
+     */
+    public abstract boolean trySkip(long n) throws IOException;
+
+    /**
+     * Sets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     *
+     * @param motorolaByteOrder <code>true</code> for Motorola/big endian, <code>false</code> for Intel/little endian
+     */
+    public void setMotorolaByteOrder(boolean motorolaByteOrder)
+    {
+        _isMotorolaByteOrder = motorolaByteOrder;
+    }
+
+    /**
+     * Gets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
+     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
+     * </ul>
+     */
+    public boolean isMotorolaByteOrder()
+    {
+        return _isMotorolaByteOrder;
+    }
+
+    /**
+     * Returns an unsigned 8-bit int calculated from the next byte of the sequence.
+     *
+     * @return the 8 bit int value, between 0 and 255
+     */
+    public short getUInt8() throws IOException
+    {
+        return (short) (getByte() & 0xFF);
+    }
+
+    /**
+     * Returns a signed 8-bit int calculated from the next byte the sequence.
+     *
+     * @return the 8 bit int value, between 0x00 and 0xFF
+     */
+    public byte getInt8() throws IOException
+    {
+        return getByte();
+    }
+
+    /**
+     * Returns an unsigned 16-bit int calculated from the next two bytes of the sequence.
+     *
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     */
+    public int getUInt16() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (getByte() << 8 & 0xFF00) |
+                   (getByte()      & 0xFF);
+        } else {
+            // Intel ordering - LSB first
+            return (getByte()      & 0xFF) |
+                   (getByte() << 8 & 0xFF00);
+        }
+    }
+
+    /**
+     * Returns a signed 16-bit int calculated from two bytes of data (MSB, LSB).
+     *
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public short getInt16() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (short) (((short)getByte() << 8 & (short)0xFF00) |
+                            ((short)getByte()      & (short)0xFF));
+        } else {
+            // Intel ordering - LSB first
+            return (short) (((short)getByte()      & (short)0xFF) |
+                            ((short)getByte() << 8 & (short)0xFF00));
+        }
+    }
+
+    /**
+     * Get a 32-bit unsigned integer from the buffer, returning it as a long.
+     *
+     * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public long getUInt32() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (((long)getByte()) << 24 & 0xFF000000L) |
+                   (((long)getByte()) << 16 & 0xFF0000L) |
+                   (((long)getByte()) << 8  & 0xFF00L) |
+                   (((long)getByte())       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (((long)getByte())       & 0xFFL) |
+                   (((long)getByte()) << 8  & 0xFF00L) |
+                   (((long)getByte()) << 16 & 0xFF0000L) |
+                   (((long)getByte()) << 24 & 0xFF000000L);
+        }
+    }
+
+    /**
+     * Returns a signed 32-bit integer from four bytes of data.
+     *
+     * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public int getInt32() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (getByte() << 24 & 0xFF000000) |
+                   (getByte() << 16 & 0xFF0000) |
+                   (getByte() << 8  & 0xFF00) |
+                   (getByte()       & 0xFF);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (getByte()       & 0xFF) |
+                   (getByte() << 8  & 0xFF00) |
+                   (getByte() << 16 & 0xFF0000) |
+                   (getByte() << 24 & 0xFF000000);
+        }
+    }
+
+    /**
+     * Get a signed 64-bit integer from the buffer.
+     *
+     * @return the 64 bit int value, between 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public long getInt64() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return ((long)getByte() << 56 & 0xFF00000000000000L) |
+                   ((long)getByte() << 48 & 0xFF000000000000L) |
+                   ((long)getByte() << 40 & 0xFF0000000000L) |
+                   ((long)getByte() << 32 & 0xFF00000000L) |
+                   ((long)getByte() << 24 & 0xFF000000L) |
+                   ((long)getByte() << 16 & 0xFF0000L) |
+                   ((long)getByte() << 8  & 0xFF00L) |
+                   ((long)getByte()       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first
+            return ((long)getByte()       & 0xFFL) |
+                   ((long)getByte() << 8  & 0xFF00L) |
+                   ((long)getByte() << 16 & 0xFF0000L) |
+                   ((long)getByte() << 24 & 0xFF000000L) |
+                   ((long)getByte() << 32 & 0xFF00000000L) |
+                   ((long)getByte() << 40 & 0xFF0000000000L) |
+                   ((long)getByte() << 48 & 0xFF000000000000L) |
+                   ((long)getByte() << 56 & 0xFF00000000000000L);
+        }
+    }
+
+    /**
+     * Gets a s15.16 fixed point float from the buffer.
+     * <p>
+     * This particular fixed point encoding has one sign bit, 15 numerator bits and 16 denominator bits.
+     *
+     * @return the floating point value
+     * @throws IOException the buffer does not contain enough bytes to service the request
+     */
+    public float getS15Fixed16() throws IOException
+    {
+        if (_isMotorolaByteOrder) {
+            float res = (getByte() & 0xFF) << 8 |
+                        (getByte() & 0xFF);
+            int d =     (getByte() & 0xFF) << 8 |
+                        (getByte() & 0xFF);
+            return (float)(res + d/65536.0);
+        } else {
+            // this particular branch is untested
+            int d =     (getByte() & 0xFF) |
+                        (getByte() & 0xFF) << 8;
+            float res = (getByte() & 0xFF) |
+                        (getByte() & 0xFF) << 8;
+            return (float)(res + d/65536.0);
+        }
+    }
+
+    public float getFloat32() throws IOException
+    {
+        return Float.intBitsToFloat(getInt32());
+    }
+
+    public double getDouble64() throws IOException
+    {
+        return Double.longBitsToDouble(getInt64());
+    }
+
+    @NotNull
+    public String getString(int bytesRequested) throws IOException
+    {
+        return new String(getBytes(bytesRequested));
+    }
+
+    @NotNull
+    public String getString(int bytesRequested, String charset) throws IOException
+    {
+        byte[] bytes = getBytes(bytesRequested);
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return new String(bytes);
+        }
+    }
+
+    /**
+     * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
+     *
+     * @param maxLengthBytes The maximum number of bytes to read.  If a zero-byte is not reached within this limit,
+     *                       reading will stop and the string will be truncated to this length.
+     * @return The read string.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public String getNullTerminatedString(int maxLengthBytes) throws IOException
+    {
+        // NOTE currently only really suited to single-byte character strings
+
+        byte[] bytes = new byte[maxLengthBytes];
+
+        // Count the number of non-null bytes
+        int length = 0;
+        while (length < bytes.length && (bytes[length] = getByte()) != '\0')
+            length++;
+
+        return new String(bytes, 0, length);
+    }
+}
diff --git a/src/com/drew/lang/StreamReader.java b/src/com/drew/lang/StreamReader.java
new file mode 100644
index 0000000..c55db50
--- /dev/null
+++ b/src/com/drew/lang/StreamReader.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class StreamReader extends SequentialReader
+{
+    @NotNull
+    private final InputStream _stream;
+
+    @SuppressWarnings("ConstantConditions")
+    public StreamReader(@NotNull InputStream stream)
+    {
+        if (stream == null)
+            throw new NullPointerException();
+
+        _stream = stream;
+    }
+
+    @Override
+    protected byte getByte() throws IOException
+    {
+        int value = _stream.read();
+        if (value == -1)
+            throw new EOFException("End of data reached.");
+        return (byte)value;
+    }
+
+    @NotNull
+    @Override
+    public byte[] getBytes(int count) throws IOException
+    {
+        byte[] bytes = new byte[count];
+        int totalBytesRead = 0;
+
+        while (totalBytesRead != count) {
+            final int bytesRead = _stream.read(bytes, totalBytesRead, count - totalBytesRead);
+            if (bytesRead == -1)
+                throw new EOFException("End of data reached.");
+            totalBytesRead += bytesRead;
+            assert(totalBytesRead <= count);
+        }
+
+        return bytes;
+    }
+
+    @Override
+    public void skip(long n) throws IOException
+    {
+        if (n < 0)
+            throw new IllegalArgumentException("n must be zero or greater.");
+
+        long skippedCount = skipInternal(n);
+
+        if (skippedCount != n)
+            throw new EOFException(String.format("Unable to skip. Requested %d bytes but skipped %d.", n, skippedCount));
+    }
+
+    @Override
+    public boolean trySkip(long n) throws IOException
+    {
+        if (n < 0)
+            throw new IllegalArgumentException("n must be zero or greater.");
+
+        return skipInternal(n) == n;
+    }
+
+    private long skipInternal(long n) throws IOException
+    {
+        // It seems that for some streams, such as BufferedInputStream, that skip can return
+        // some smaller number than was requested. So loop until we either skip enough, or
+        // InputStream.skip returns zero.
+        //
+        // See http://stackoverflow.com/questions/14057720/robust-skipping-of-data-in-a-java-io-inputstream-and-its-subtypes
+        //
+        long skippedTotal = 0;
+        while (skippedTotal != n) {
+            long skipped = _stream.skip(n - skippedTotal);
+            assert(skipped >= 0);
+            skippedTotal += skipped;
+            if (skipped == 0)
+                break;
+        }
+        return skippedTotal;
+    }
+}
diff --git a/src/com/drew/lang/StringUtil.java b/src/com/drew/lang/StringUtil.java
new file mode 100755
index 0000000..35c3a9d
--- /dev/null
+++ b/src/com/drew/lang/StringUtil.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class StringUtil
+{
+    @NotNull
+    public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter)
+    {
+        int capacity = 0;
+        int delimLength = delimiter.length();
+
+        Iterator<? extends CharSequence> iter = strings.iterator();
+        if (iter.hasNext())
+            capacity += iter.next().length() + delimLength;
+
+        StringBuilder buffer = new StringBuilder(capacity);
+        iter = strings.iterator();
+        if (iter.hasNext()) {
+            buffer.append(iter.next());
+            while (iter.hasNext()) {
+                buffer.append(delimiter);
+                buffer.append(iter.next());
+            }
+        }
+        return buffer.toString();
+    }
+
+    @NotNull
+    public static <T extends CharSequence> String join(@NotNull T[] strings, @NotNull String delimiter)
+    {
+        int capacity = 0;
+        int delimLength = delimiter.length();
+        for (T value : strings)
+            capacity += value.length() + delimLength;
+
+        StringBuilder buffer = new StringBuilder(capacity);
+        boolean first = true;
+        for (T value : strings) {
+            if (!first) {
+                buffer.append(delimiter);
+            } else {
+                first = false;
+            }
+            buffer.append(value);
+        }
+        return buffer.toString();
+    }
+
+    @NotNull
+    public static String fromStream(@NotNull InputStream stream) throws IOException
+    {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+        StringBuilder sb = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            sb.append(line);
+        }
+        return sb.toString();
+    }
+
+    public static int compare(@Nullable String s1, @Nullable String s2)
+    {
+        boolean null1 = s1 == null;
+        boolean null2 = s2 == null;
+
+        if (null1 && null2) {
+            return 0;
+        } else if (null1) {
+            return -1;
+        } else if (null2) {
+            return 1;
+        } else {
+            return s1.compareTo(s2);
+        }
+    }
+
+    @NotNull
+    public static String urlEncode(@NotNull String name)
+    {
+        // Sufficient for now, it seems
+        return name.replace(" ", "%20");
+    }
+}
diff --git a/src/com/drew/lang/annotations/NotNull.java b/src/com/drew/lang/annotations/NotNull.java
new file mode 100755
index 0000000..4bba728
--- /dev/null
+++ b/src/com/drew/lang/annotations/NotNull.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang.annotations;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public @interface NotNull
+{
+}
diff --git a/src/com/drew/lang/annotations/Nullable.java b/src/com/drew/lang/annotations/Nullable.java
new file mode 100755
index 0000000..e37e45f
--- /dev/null
+++ b/src/com/drew/lang/annotations/Nullable.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang.annotations;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public @interface Nullable
+{
+}
diff --git a/src/com/drew/lang/annotations/SuppressWarnings.java b/src/com/drew/lang/annotations/SuppressWarnings.java
new file mode 100755
index 0000000..8f18f1b
--- /dev/null
+++ b/src/com/drew/lang/annotations/SuppressWarnings.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2011 Andreas Ziermann
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.lang.annotations;
+
+/**
+ * Used to suppress specific code analysis warnings produced by the Findbugs tool.
+ *
+ * @author Andreas Ziermann
+ */
+public @interface SuppressWarnings
+{
+    /**
+     * The name of the warning to be suppressed.
+     * @return The name of the warning to be suppressed.
+     */
+    @NotNull String value();
+
+    /**
+     * An explanation of why it is valid to suppress the warning in a particular situation/context.
+     * @return An explanation of why it is valid to suppress the warning in a particular situation/context.
+     */
+    @NotNull String justification();
+}
diff --git a/src/com/drew/lang/annotations/package.html b/src/com/drew/lang/annotations/package.html
new file mode 100644
index 0000000..c648601
--- /dev/null
+++ b/src/com/drew/lang/annotations/package.html
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA
+to provide design-time warnings about potential run-time errors.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/lang/package.html b/src/com/drew/lang/package.html
new file mode 100644
index 0000000..aa6d0d0
--- /dev/null
+++ b/src/com/drew/lang/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes of generic utility.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/Age.java b/src/com/drew/metadata/Age.java
new file mode 100755
index 0000000..4022214
--- /dev/null
+++ b/src/com/drew/metadata/Age.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Represents an age in years, months, days, hours, minutes and seconds.
+ * <p>
+ * Used by certain Panasonic cameras which have face recognition features.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class Age
+{
+    private final int _years;
+    private final int _months;
+    private final int _days;
+    private final int _hours;
+    private final int _minutes;
+    private final int _seconds;
+
+    /**
+     * Parses an age object from the string format used by Panasonic cameras:
+     * <code>0031:07:15 00:00:00</code>
+     *
+     * @param s The String in format <code>0031:07:15 00:00:00</code>.
+     * @return The parsed Age object, or null if the value could not be parsed
+     */
+    @Nullable
+    public static Age fromPanasonicString(@NotNull String s)
+    {
+        if (s == null)
+            throw new NullPointerException();
+
+        if (s.length() != 19 || s.startsWith("9999:99:99"))
+            return null;
+
+        try {
+            int years = Integer.parseInt(s.substring(0, 4));
+            int months = Integer.parseInt(s.substring(5, 7));
+            int days = Integer.parseInt(s.substring(8, 10));
+            int hours = Integer.parseInt(s.substring(11, 13));
+            int minutes = Integer.parseInt(s.substring(14, 16));
+            int seconds = Integer.parseInt(s.substring(17, 19));
+
+            return new Age(years, months, days, hours, minutes, seconds);
+        }
+        catch (NumberFormatException ignored)
+        {
+            return null;
+        }
+    }
+
+    public Age(int years, int months, int days, int hours, int minutes, int seconds)
+    {
+        _years = years;
+        _months = months;
+        _days = days;
+        _hours = hours;
+        _minutes = minutes;
+        _seconds = seconds;
+    }
+
+    public int getYears()
+    {
+        return _years;
+    }
+
+    public int getMonths()
+    {
+        return _months;
+    }
+
+    public int getDays()
+    {
+        return _days;
+    }
+
+    public int getHours()
+    {
+        return _hours;
+    }
+
+    public int getMinutes()
+    {
+        return _minutes;
+    }
+
+    public int getSeconds()
+    {
+        return _seconds;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("%04d:%02d:%02d %02d:%02d:%02d", _years, _months, _days, _hours, _minutes, _seconds);
+    }
+
+    public String toFriendlyString()
+    {
+        StringBuilder result = new StringBuilder();
+        appendAgePart(result, _years, "year");
+        appendAgePart(result, _months, "month");
+        appendAgePart(result, _days, "day");
+        appendAgePart(result, _hours, "hour");
+        appendAgePart(result, _minutes, "minute");
+        appendAgePart(result, _seconds, "second");
+        return result.toString();
+    }
+
+    private static void appendAgePart(StringBuilder result, final int num, final String singularName)
+    {
+        if (num == 0)
+            return;
+        if (result.length()!=0)
+            result.append(' ');
+        result.append(num).append(' ').append(singularName);
+        if (num != 1)
+            result.append('s');
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Age age = (Age)o;
+
+        if (_days != age._days) return false;
+        if (_hours != age._hours) return false;
+        if (_minutes != age._minutes) return false;
+        if (_months != age._months) return false;
+        if (_seconds != age._seconds) return false;
+        if (_years != age._years) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = _years;
+        result = 31 * result + _months;
+        result = 31 * result + _days;
+        result = 31 * result + _hours;
+        result = 31 * result + _minutes;
+        result = 31 * result + _seconds;
+        return result;
+    }
+}
diff --git a/src/com/drew/metadata/DefaultTagDescriptor.java b/src/com/drew/metadata/DefaultTagDescriptor.java
new file mode 100644
index 0000000..ed157bc
--- /dev/null
+++ b/src/com/drew/metadata/DefaultTagDescriptor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * A default implementation of the abstract TagDescriptor.  As this class is not coded with awareness of any metadata
+ * tags, it simply reports tag names using the format 'Unknown tag 0x00' (with the corresponding tag number in hex)
+ * and gives descriptions using the default string representation of the value.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class DefaultTagDescriptor extends TagDescriptor<Directory>
+{
+    public DefaultTagDescriptor(@NotNull Directory directory)
+    {
+        super(directory);
+    }
+
+    /**
+     * Gets a best-effort tag name using the format 'Unknown tag 0x00' (with the corresponding tag type in hex).
+     * @param tagType the tag type identifier.
+     * @return a string representation of the tag name.
+     */
+    @NotNull
+    public String getTagName(int tagType)
+    {
+        String hex = Integer.toHexString(tagType).toUpperCase();
+        while (hex.length() < 4) hex = "0" + hex;
+        return "Unknown tag 0x" + hex;
+    }
+}
diff --git a/src/com/drew/metadata/Directory.java b/src/com/drew/metadata/Directory.java
new file mode 100644
index 0000000..cc6d791
--- /dev/null
+++ b/src/com/drew/metadata/Directory.java
@@ -0,0 +1,958 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.lang.annotations.SuppressWarnings;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Array;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Abstract base class for all directory implementations, having methods for getting and setting tag values of various
+ * data types.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class Directory
+{
+    /** Map of values hashed by type identifiers. */
+    @NotNull
+    protected final Map<Integer, Object> _tagMap = new HashMap<Integer, Object>();
+
+    /**
+     * A convenient list holding tag values in the order in which they were stored.
+     * This is used for creation of an iterator, and for counting the number of
+     * defined tags.
+     */
+    @NotNull
+    protected final Collection<Tag> _definedTagList = new ArrayList<Tag>();
+
+    @NotNull
+    private final Collection<String> _errorList = new ArrayList<String>(4);
+
+    /** The descriptor used to interpret tag values. */
+    protected TagDescriptor _descriptor;
+
+// ABSTRACT METHODS
+
+    /**
+     * Provides the name of the directory, for display purposes.  E.g. <code>Exif</code>
+     *
+     * @return the name of the directory
+     */
+    @NotNull
+    public abstract String getName();
+
+    /**
+     * Provides the map of tag names, hashed by tag type identifier.
+     *
+     * @return the map of tag names
+     */
+    @NotNull
+    protected abstract HashMap<Integer, String> getTagNameMap();
+
+    protected Directory()
+    {}
+
+// VARIOUS METHODS
+
+    /**
+     * Gets a value indicating whether the directory is empty, meaning it contains no errors and no tag values.
+     */
+    public boolean isEmpty()
+    {
+        return _errorList.isEmpty() && _definedTagList.isEmpty();
+    }
+
+    /**
+     * Indicates whether the specified tag type has been set.
+     *
+     * @param tagType the tag type to check for
+     * @return true if a value exists for the specified tag type, false if not
+     */
+    @java.lang.SuppressWarnings({ "UnnecessaryBoxing" })
+    public boolean containsTag(int tagType)
+    {
+        return _tagMap.containsKey(Integer.valueOf(tagType));
+    }
+
+    /**
+     * Returns an Iterator of Tag instances that have been set in this Directory.
+     *
+     * @return an Iterator of Tag instances
+     */
+    @NotNull
+    public Collection<Tag> getTags()
+    {
+        return Collections.unmodifiableCollection(_definedTagList);
+    }
+
+    /**
+     * Returns the number of tags set in this Directory.
+     *
+     * @return the number of tags set in this Directory
+     */
+    public int getTagCount()
+    {
+        return _definedTagList.size();
+    }
+
+    /**
+     * Sets the descriptor used to interpret tag values.
+     *
+     * @param descriptor the descriptor used to interpret tag values
+     */
+    @java.lang.SuppressWarnings({ "ConstantConditions" })
+    public void setDescriptor(@NotNull TagDescriptor descriptor)
+    {
+        if (descriptor == null)
+            throw new NullPointerException("cannot set a null descriptor");
+        _descriptor = descriptor;
+    }
+
+    /**
+     * Registers an error message with this directory.
+     *
+     * @param message an error message.
+     */
+    public void addError(@NotNull String message)
+    {
+        _errorList.add(message);
+    }
+
+    /**
+     * Gets a value indicating whether this directory has any error messages.
+     *
+     * @return true if the directory contains errors, otherwise false
+     */
+    public boolean hasErrors()
+    {
+        return _errorList.size() > 0;
+    }
+
+    /**
+     * Used to iterate over any error messages contained in this directory.
+     *
+     * @return an iterable collection of error message strings.
+     */
+    @NotNull
+    public Iterable<String> getErrors()
+    {
+        return Collections.unmodifiableCollection(_errorList);
+    }
+
+    /** Returns the count of error messages in this directory. */
+    public int getErrorCount()
+    {
+        return _errorList.size();
+    }
+
+// TAG SETTERS
+
+    /**
+     * Sets an <code>int</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as an int
+     */
+    public void setInt(int tagType, int value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets an <code>int[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param ints    the int array to store
+     */
+    public void setIntArray(int tagType, @NotNull int[] ints)
+    {
+        setObjectArray(tagType, ints);
+    }
+
+    /**
+     * Sets a <code>float</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a float
+     */
+    public void setFloat(int tagType, float value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>float[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param floats  the float array to store
+     */
+    public void setFloatArray(int tagType, @NotNull float[] floats)
+    {
+        setObjectArray(tagType, floats);
+    }
+
+    /**
+     * Sets a <code>double</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a double
+     */
+    public void setDouble(int tagType, double value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>double[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param doubles the double array to store
+     */
+    public void setDoubleArray(int tagType, @NotNull double[] doubles)
+    {
+        setObjectArray(tagType, doubles);
+    }
+
+    /**
+     * Sets a <code>String</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a String
+     */
+    @java.lang.SuppressWarnings({ "ConstantConditions" })
+    public void setString(int tagType, @NotNull String value)
+    {
+        if (value == null)
+            throw new NullPointerException("cannot set a null String");
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>String[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param strings the String array to store
+     */
+    public void setStringArray(int tagType, @NotNull String[] strings)
+    {
+        setObjectArray(tagType, strings);
+    }
+
+    /**
+     * Sets a <code>boolean</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a boolean
+     */
+    public void setBoolean(int tagType, boolean value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>long</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a long
+     */
+    public void setLong(int tagType, long value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>java.util.Date</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a java.util.Date
+     */
+    public void setDate(int tagType, @NotNull java.util.Date value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>Rational</code> value for the specified tag.
+     *
+     * @param tagType  the tag's value as an int
+     * @param rational rational number
+     */
+    public void setRational(int tagType, @NotNull Rational rational)
+    {
+        setObject(tagType, rational);
+    }
+
+    /**
+     * Sets a <code>Rational[]</code> (array) for the specified tag.
+     *
+     * @param tagType   the tag identifier
+     * @param rationals the Rational array to store
+     */
+    public void setRationalArray(int tagType, @NotNull Rational[] rationals)
+    {
+        setObjectArray(tagType, rationals);
+    }
+
+    /**
+     * Sets a <code>byte[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param bytes   the byte array to store
+     */
+    public void setByteArray(int tagType, @NotNull byte[] bytes)
+    {
+        setObjectArray(tagType, bytes);
+    }
+
+    /**
+     * Sets a <code>Object</code> for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag
+     * @throws NullPointerException if value is <code>null</code>
+     */
+    @java.lang.SuppressWarnings( { "ConstantConditions", "UnnecessaryBoxing" })
+    public void setObject(int tagType, @NotNull Object value)
+    {
+        if (value == null)
+            throw new NullPointerException("cannot set a null object");
+
+        if (!_tagMap.containsKey(Integer.valueOf(tagType))) {
+            _definedTagList.add(new Tag(tagType, this));
+        }
+//        else {
+//            final Object oldValue = _tagMap.get(tagType);
+//            if (!oldValue.equals(value))
+//                addError(String.format("Overwritten tag 0x%s (%s).  Old=%s, New=%s", Integer.toHexString(tagType), getTagName(tagType), oldValue, value));
+//        }
+        _tagMap.put(tagType, value);
+    }
+
+    /**
+     * Sets an array <code>Object</code> for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param array   the array of values for the specified tag
+     */
+    public void setObjectArray(int tagType, @NotNull Object array)
+    {
+        // for now, we don't do anything special -- this method might be a candidate for removal once the dust settles
+        setObject(tagType, array);
+    }
+
+// TAG GETTERS
+
+    /**
+     * Returns the specified tag's value as an int, if possible.  Every attempt to represent the tag's value as an int
+     * is taken.  Here is a list of the action taken depending upon the tag's original type:
+     * <ul>
+     * <li> int - Return unchanged.
+     * <li> Number - Return an int value (real numbers are truncated).
+     * <li> Rational - Truncate any fractional part and returns remaining int.
+     * <li> String - Attempt to parse string as an int.  If this fails, convert the char[] to an int (using shifts and OR).
+     * <li> Rational[] - Return int value of first item in array.
+     * <li> byte[] - Return int value of first item in array.
+     * <li> int[] - Return int value of first item in array.
+     * </ul>
+     *
+     * @throws MetadataException if no value exists for tagType or if it cannot be converted to an int.
+     */
+    public int getInt(int tagType) throws MetadataException
+    {
+        Integer integer = getInteger(tagType);
+        if (integer!=null)
+            return integer;
+
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to int.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /**
+     * Returns the specified tag's value as an Integer, if possible.  Every attempt to represent the tag's value as an
+     * Integer is taken.  Here is a list of the action taken depending upon the tag's original type:
+     * <ul>
+     * <li> int - Return unchanged
+     * <li> Number - Return an int value (real numbers are truncated)
+     * <li> Rational - Truncate any fractional part and returns remaining int
+     * <li> String - Attempt to parse string as an int.  If this fails, convert the char[] to an int (using shifts and OR)
+     * <li> Rational[] - Return int value of first item in array if length > 0
+     * <li> byte[] - Return int value of first item in array if length > 0
+     * <li> int[] - Return int value of first item in array if length > 0
+     * </ul>
+     *
+     * If the value is not found or cannot be converted to int, <code>null</code> is returned.
+     */
+    @Nullable
+    public Integer getInteger(int tagType)
+    {
+        Object o = getObject(tagType);
+
+        if (o == null)
+            return null;
+
+        if (o instanceof Number) {
+            return ((Number)o).intValue();
+        } else if (o instanceof String) {
+            try {
+                return Integer.parseInt((String)o);
+            } catch (NumberFormatException nfe) {
+                // convert the char array to an int
+                String s = (String)o;
+                byte[] bytes = s.getBytes();
+                long val = 0;
+                for (byte aByte : bytes) {
+                    val = val << 8;
+                    val += (aByte & 0xff);
+                }
+                return (int)val;
+            }
+        } else if (o instanceof Rational[]) {
+            Rational[] rationals = (Rational[])o;
+            if (rationals.length == 1)
+                return rationals[0].intValue();
+        } else if (o instanceof byte[]) {
+            byte[] bytes = (byte[])o;
+            if (bytes.length == 1)
+                return (int)bytes[0];
+        } else if (o instanceof int[]) {
+            int[] ints = (int[])o;
+            if (ints.length == 1)
+                return ints[0];
+        }
+        return null;
+    }
+
+    /**
+     * Gets the specified tag's value as a String array, if possible.  Only supported
+     * where the tag is set as String[], String, int[], byte[] or Rational[].
+     *
+     * @param tagType the tag identifier
+     * @return the tag's value as an array of Strings. If the value is unset or cannot be converted, <code>null</code> is returned.
+     */
+    @Nullable
+    public String[] getStringArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String[])
+            return (String[])o;
+        if (o instanceof String)
+            return new String[] { (String)o };
+        if (o instanceof int[]) {
+            int[] ints = (int[])o;
+            String[] strings = new String[ints.length];
+            for (int i = 0; i < strings.length; i++)
+                strings[i] = Integer.toString(ints[i]);
+            return strings;
+        } else if (o instanceof byte[]) {
+            byte[] bytes = (byte[])o;
+            String[] strings = new String[bytes.length];
+            for (int i = 0; i < strings.length; i++)
+                strings[i] = Byte.toString(bytes[i]);
+            return strings;
+        } else if (o instanceof Rational[]) {
+            Rational[] rationals = (Rational[])o;
+            String[] strings = new String[rationals.length];
+            for (int i = 0; i < strings.length; i++)
+                strings[i] = rationals[i].toSimpleString(false);
+            return strings;
+        }
+        return null;
+    }
+
+    /**
+     * Gets the specified tag's value as an int array, if possible.  Only supported
+     * where the tag is set as String, Integer, int[], byte[] or Rational[].
+     *
+     * @param tagType the tag identifier
+     * @return the tag's value as an int array
+     */
+    @Nullable
+    public int[] getIntArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof int[])
+            return (int[])o;
+        if (o instanceof Rational[]) {
+            Rational[] rationals = (Rational[])o;
+            int[] ints = new int[rationals.length];
+            for (int i = 0; i < ints.length; i++) {
+                ints[i] = rationals[i].intValue();
+            }
+            return ints;
+        }
+        if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            int[] ints = new int[shorts.length];
+            for (int i = 0; i < shorts.length; i++) {
+                ints[i] = shorts[i];
+            }
+            return ints;
+        }
+        if (o instanceof byte[]) {
+            byte[] bytes = (byte[])o;
+            int[] ints = new int[bytes.length];
+            for (int i = 0; i < bytes.length; i++) {
+                ints[i] = bytes[i];
+            }
+            return ints;
+        }
+        if (o instanceof CharSequence) {
+            CharSequence str = (CharSequence)o;
+            int[] ints = new int[str.length()];
+            for (int i = 0; i < str.length(); i++) {
+                ints[i] = str.charAt(i);
+            }
+            return ints;
+        }
+        if (o instanceof Integer)
+            return new int[] { (Integer)o };
+
+        return null;
+    }
+
+    /**
+     * Gets the specified tag's value as an byte array, if possible.  Only supported
+     * where the tag is set as String, Integer, int[], byte[] or Rational[].
+     *
+     * @param tagType the tag identifier
+     * @return the tag's value as a byte array
+     */
+    @Nullable
+    public byte[] getByteArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null) {
+            return null;
+        } else if (o instanceof Rational[]) {
+            Rational[] rationals = (Rational[])o;
+            byte[] bytes = new byte[rationals.length];
+            for (int i = 0; i < bytes.length; i++) {
+                bytes[i] = rationals[i].byteValue();
+            }
+            return bytes;
+        } else if (o instanceof byte[]) {
+            return (byte[])o;
+        } else if (o instanceof int[]) {
+            int[] ints = (int[])o;
+            byte[] bytes = new byte[ints.length];
+            for (int i = 0; i < ints.length; i++) {
+                bytes[i] = (byte)ints[i];
+            }
+            return bytes;
+        } else if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            byte[] bytes = new byte[shorts.length];
+            for (int i = 0; i < shorts.length; i++) {
+                bytes[i] = (byte)shorts[i];
+            }
+            return bytes;
+        } else if (o instanceof CharSequence) {
+            CharSequence str = (CharSequence)o;
+            byte[] bytes = new byte[str.length()];
+            for (int i = 0; i < str.length(); i++) {
+                bytes[i] = (byte)str.charAt(i);
+            }
+            return bytes;
+        }
+        if (o instanceof Integer)
+            return new byte[] { ((Integer)o).byteValue() };
+
+        return null;
+    }
+
+    /** Returns the specified tag's value as a double, if possible. */
+    public double getDouble(int tagType) throws MetadataException
+    {
+        Double value = getDoubleObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a double.  It is of type '" + o.getClass() + "'.");
+    }
+    /** Returns the specified tag's value as a Double.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Double getDoubleObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String) {
+            try {
+                return Double.parseDouble((String)o);
+            } catch (NumberFormatException nfe) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
+            return ((Number)o).doubleValue();
+
+        return null;
+    }
+
+    /** Returns the specified tag's value as a float, if possible. */
+    public float getFloat(int tagType) throws MetadataException
+    {
+        Float value = getFloatObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a float.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /** Returns the specified tag's value as a float.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Float getFloatObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String) {
+            try {
+                return Float.parseFloat((String)o);
+            } catch (NumberFormatException nfe) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
+            return ((Number)o).floatValue();
+        return null;
+    }
+
+    /** Returns the specified tag's value as a long, if possible. */
+    public long getLong(int tagType) throws MetadataException
+    {
+        Long value = getLongObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a long.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /** Returns the specified tag's value as a long.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Long getLongObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String) {
+            try {
+                return Long.parseLong((String)o);
+            } catch (NumberFormatException nfe) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
+            return ((Number)o).longValue();
+        return null;
+    }
+
+    /** Returns the specified tag's value as a boolean, if possible. */
+    public boolean getBoolean(int tagType) throws MetadataException
+    {
+        Boolean value = getBooleanObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a boolean.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /** Returns the specified tag's value as a boolean.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    @SuppressWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "keep API interface consistent")
+    public Boolean getBooleanObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof Boolean)
+            return (Boolean)o;
+        if (o instanceof String) {
+            try {
+                return Boolean.getBoolean((String)o);
+            } catch (NumberFormatException nfe) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
+            return (((Number)o).doubleValue() != 0);
+        return null;
+    }
+
+    /**
+     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
+     * <p>
+     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
+     * the current {@link TimeZone}.  If the {@link TimeZone} is known, call the overload that accepts one as an argument.
+     */
+    @Nullable
+    public java.util.Date getDate(int tagType)
+    {
+        return getDate(tagType, null);
+    }
+
+    /**
+     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
+     * <p>
+     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
+     * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).  Note that this parameter
+     * is only considered if the underlying value is a string and parsing occurs, otherwise it has no effect.
+     */
+    @Nullable
+    public java.util.Date getDate(int tagType, @Nullable TimeZone timeZone)
+    {
+        Object o = getObject(tagType);
+
+        if (o == null)
+            return null;
+
+        if (o instanceof java.util.Date)
+            return (java.util.Date)o;
+
+        if (o instanceof String) {
+            // This seems to cover all known Exif date strings
+            // Note that "    :  :     :  :  " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
+            String datePatterns[] = {
+                    "yyyy:MM:dd HH:mm:ss",
+                    "yyyy:MM:dd HH:mm",
+                    "yyyy-MM-dd HH:mm:ss",
+                    "yyyy-MM-dd HH:mm",
+                    "yyyy.MM.dd HH:mm:ss",
+                    "yyyy.MM.dd HH:mm" };
+            String dateString = (String)o;
+            for (String datePattern : datePatterns) {
+                try {
+                    DateFormat parser = new SimpleDateFormat(datePattern);
+                    if (timeZone != null)
+                        parser.setTimeZone(timeZone);
+                    else
+                        parser.setTimeZone(TimeZone.getTimeZone("GMT")); // don't interpret zone time
+
+                    return parser.parse(dateString);
+                } catch (ParseException ex) {
+                    // simply try the next pattern
+                }
+            }
+        }
+        return null;
+    }
+
+    /** Returns the specified tag's value as a Rational.  If the value is unset or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Rational getRational(int tagType)
+    {
+        Object o = getObject(tagType);
+
+        if (o == null)
+            return null;
+
+        if (o instanceof Rational)
+            return (Rational)o;
+        if (o instanceof Integer)
+            return new Rational((Integer)o, 1);
+        if (o instanceof Long)
+            return new Rational((Long)o, 1);
+
+        // NOTE not doing conversions for real number types
+
+        return null;
+    }
+
+    /** Returns the specified tag's value as an array of Rational.  If the value is unset or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Rational[] getRationalArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+
+        if (o instanceof Rational[])
+            return (Rational[])o;
+
+        return null;
+    }
+
+    /**
+     * Returns the specified tag's value as a String.  This value is the 'raw' value.  A more presentable decoding
+     * of this value may be obtained from the corresponding Descriptor.
+     *
+     * @return the String representation of the tag's value, or
+     *         <code>null</code> if the tag hasn't been defined.
+     */
+    @Nullable
+    public String getString(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+
+        if (o instanceof Rational)
+            return ((Rational)o).toSimpleString(true);
+
+        if (o.getClass().isArray()) {
+            // handle arrays of objects and primitives
+            int arrayLength = Array.getLength(o);
+            final Class<?> componentType = o.getClass().getComponentType();
+            boolean isObjectArray = Object.class.isAssignableFrom(componentType);
+            boolean isFloatArray = componentType.getName().equals("float");
+            boolean isDoubleArray = componentType.getName().equals("double");
+            boolean isIntArray = componentType.getName().equals("int");
+            boolean isLongArray = componentType.getName().equals("long");
+            boolean isByteArray = componentType.getName().equals("byte");
+            boolean isShortArray = componentType.getName().equals("short");
+            StringBuilder string = new StringBuilder();
+            for (int i = 0; i < arrayLength; i++) {
+                if (i != 0)
+                    string.append(' ');
+                if (isObjectArray)
+                    string.append(Array.get(o, i).toString());
+                else if (isIntArray)
+                    string.append(Array.getInt(o, i));
+                else if (isShortArray)
+                    string.append(Array.getShort(o, i));
+                else if (isLongArray)
+                    string.append(Array.getLong(o, i));
+                else if (isFloatArray)
+                    string.append(Array.getFloat(o, i));
+                else if (isDoubleArray)
+                    string.append(Array.getDouble(o, i));
+                else if (isByteArray)
+                    string.append(Array.getByte(o, i));
+                else
+                    addError("Unexpected array component type: " + componentType.getName());
+            }
+            return string.toString();
+        }
+
+        // Note that several cameras leave trailing spaces (Olympus, Nikon) but this library is intended to show
+        // the actual data within the file.  It is not inconceivable that whitespace may be significant here, so we
+        // do not trim.  Also, if support is added for writing data back to files, this may cause issues.
+        // We leave trimming to the presentation layer.
+        return o.toString();
+    }
+
+    @Nullable
+    public String getString(int tagType, String charset)
+    {
+        byte[] bytes = getByteArray(tagType);
+        if (bytes==null)
+            return null;
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the object hashed for the particular tag type specified, if available.
+     *
+     * @param tagType the tag type identifier
+     * @return the tag's value as an Object if available, else <code>null</code>
+     */
+    @java.lang.SuppressWarnings({ "UnnecessaryBoxing" })
+    @Nullable
+    public Object getObject(int tagType)
+    {
+        return _tagMap.get(Integer.valueOf(tagType));
+    }
+
+// OTHER METHODS
+
+    /**
+     * Returns the name of a specified tag as a String.
+     *
+     * @param tagType the tag type identifier
+     * @return the tag's name as a String
+     */
+    @NotNull
+    public String getTagName(int tagType)
+    {
+        HashMap<Integer, String> nameMap = getTagNameMap();
+        if (!nameMap.containsKey(tagType)) {
+            String hex = Integer.toHexString(tagType);
+            while (hex.length() < 4) {
+                hex = "0" + hex;
+            }
+            return "Unknown tag (0x" + hex + ")";
+        }
+        return nameMap.get(tagType);
+    }
+
+    /**
+     * Gets whether the specified tag is known by the directory and has a name.
+     *
+     * @param tagType the tag type identifier
+     * @return whether this directory has a name for the specified tag
+     */
+    public boolean hasTagName(int tagType)
+    {
+        return getTagNameMap().containsKey(tagType);
+    }
+
+    /**
+     * Provides a description of a tag's value using the descriptor set by
+     * <code>setDescriptor(Descriptor)</code>.
+     *
+     * @param tagType the tag type identifier
+     * @return the tag value's description as a String
+     */
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        assert(_descriptor != null);
+        return _descriptor.getDescription(tagType);
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("%s Directory (%d %s)",
+            getName(),
+            _tagMap.size(),
+            _tagMap.size() == 1
+                ? "tag"
+                : "tags");
+    }
+}
diff --git a/src/com/drew/metadata/Face.java b/src/com/drew/metadata/Face.java
new file mode 100755
index 0000000..1cf5a4a
--- /dev/null
+++ b/src/com/drew/metadata/Face.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Class to hold information about a detected or recognized face in a photo.
+ * <p>
+ * When a face is <em>detected</em>, the camera believes that a face is present at a given location in
+ * the image, but is not sure whose face it is.  When a face is <em>recognised</em>, then the face is
+ * both detected and identified as belonging to a known person.
+ *
+ * @author Philipp Sandhaus, Drew Noakes
+ */
+public class Face
+{
+    private final int _x;
+    private final int _y;
+    private final int _width;
+    private final int _height;
+    @Nullable
+    private final String _name;
+    @Nullable
+    private final Age _age;
+
+    public Face(int x, int y, int width, int height, @Nullable String name, @Nullable Age age)
+    {
+        _x = x;
+        _y = y;
+        _width = width;
+        _height = height;
+        _name = name;
+        _age = age;
+    }
+
+    public int getX()
+    {
+        return _x;
+    }
+
+    public int getY()
+    {
+        return _y;
+    }
+
+    public int getWidth()
+    {
+        return _width;
+    }
+
+    public int getHeight()
+    {
+        return _height;
+    }
+
+    @Nullable
+    public String getName()
+    {
+        return _name;
+    }
+
+    @Nullable
+    public Age getAge()
+    {
+        return _age;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Face face = (Face)o;
+
+        if (_height != face._height) return false;
+        if (_width != face._width) return false;
+        if (_x != face._x) return false;
+        if (_y != face._y) return false;
+        if (_age != null ? !_age.equals(face._age) : face._age != null) return false;
+        if (_name != null ? !_name.equals(face._name) : face._name != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = _x;
+        result = 31 * result + _y;
+        result = 31 * result + _width;
+        result = 31 * result + _height;
+        result = 31 * result + (_name != null ? _name.hashCode() : 0);
+        result = 31 * result + (_age != null ? _age.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    @NotNull
+    public String toString()
+    {
+        StringBuilder result = new StringBuilder();
+        result.append("x: ").append(_x);
+        result.append(" y: ").append(_y);
+        result.append(" width: ").append(_width);
+        result.append(" height: ").append(_height);
+        if (_name != null)
+            result.append(" name: ").append(_name);
+        if (_age != null)
+            result.append(" age: ").append(_age.toFriendlyString());
+        return result.toString();
+    }
+}
diff --git a/src/com/drew/metadata/Metadata.java b/src/com/drew/metadata/Metadata.java
new file mode 100644
index 0000000..439fea8
--- /dev/null
+++ b/src/com/drew/metadata/Metadata.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.util.*;
+
+/**
+ * A top-level object that holds the metadata values extracted from an image.
+ * <p>
+ * Metadata objects may contain zero or more {@link Directory} objects.  Each directory may contain zero or more tags
+ * with corresponding values.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class Metadata
+{
+    @NotNull
+    private final Map<Class<? extends Directory>,Collection<Directory>> _directoryListByClass = new HashMap<Class<? extends Directory>, Collection<Directory>>();
+
+    /**
+     * Returns an iterable set of the {@link Directory} instances contained in this metadata collection.
+     *
+     * @return an iterable set of directories
+     */
+    @NotNull
+    public Iterable<Directory> getDirectories()
+    {
+        return new DirectoryIterable(_directoryListByClass);
+    }
+
+    @Nullable
+    public <T extends Directory> Collection<T> getDirectoriesOfType(Class<T> type)
+    {
+        return (Collection<T>)_directoryListByClass.get(type);
+    }
+
+    /**
+     * Returns the count of directories in this metadata collection.
+     *
+     * @return the number of unique directory types set for this metadata collection
+     */
+    public int getDirectoryCount()
+    {
+        int count = 0;
+        for (Map.Entry<Class<? extends Directory>,Collection<Directory>> pair : _directoryListByClass.entrySet())
+            count += pair.getValue().size();
+        return count;
+    }
+
+    /**
+     * Adds a directory to this metadata collection.
+     *
+     * @param directory the {@link Directory} to add into this metadata collection.
+     */
+    public <T extends Directory> void addDirectory(@NotNull T directory)
+    {
+        getOrCreateDirectoryList(directory.getClass()).add(directory);
+    }
+
+    /**
+     * Gets the first {@link Directory} of the specified type contained within this metadata collection.
+     * If no instances of this type are present, <code>null</code> is returned.
+     *
+     * @param type the Directory type
+     * @param <T> the Directory type
+     * @return the first Directory of type T in this metadata collection, or <code>null</code> if none exist
+     */
+    @Nullable
+    @SuppressWarnings("unchecked")
+    public <T extends Directory> T getFirstDirectoryOfType(@NotNull Class<T> type)
+    {
+        // We suppress the warning here as the code asserts a map signature of Class<T>,T.
+        // So after get(Class<T>) it is for sure the result is from type T.
+
+        Collection<Directory> list = getDirectoryList(type);
+
+        if (list == null || list.isEmpty())
+            return null;
+
+        return (T)list.iterator().next();
+    }
+
+    /**
+     * Indicates whether an instance of the given directory type exists in this Metadata instance.
+     *
+     * @param type the {@link Directory} type
+     * @return <code>true</code> if a {@link Directory} of the specified type exists, otherwise <code>false</code>
+     */
+    public boolean containsDirectoryOfType(Class<? extends Directory> type)
+    {
+        Collection<Directory> list = getDirectoryList(type);
+        return list != null && !list.isEmpty();
+    }
+
+    /**
+     * Indicates whether any errors were reported during the reading of metadata values.
+     * This value will be true if Directory.hasErrors() is true for one of the contained {@link Directory} objects.
+     *
+     * @return whether one of the contained directories has an error
+     */
+    public boolean hasErrors()
+    {
+        for (Directory directory : getDirectories()) {
+            if (directory.hasErrors())
+                return true;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString()
+    {
+        int count = getDirectoryCount();
+        return String.format("Metadata (%d %s)",
+            count,
+            count == 1
+                ? "directory"
+                : "directories");
+    }
+
+    @Nullable
+    private <T extends Directory> Collection<Directory> getDirectoryList(@NotNull Class<T> type)
+    {
+        return _directoryListByClass.get(type);
+    }
+
+    @NotNull
+    private <T extends Directory> Collection<Directory> getOrCreateDirectoryList(@NotNull Class<T> type)
+    {
+        Collection<Directory> collection = getDirectoryList(type);
+        if (collection != null)
+            return collection;
+        collection = new ArrayList<Directory>();
+        _directoryListByClass.put(type, collection);
+        return collection;
+    }
+
+    private static class DirectoryIterable implements Iterable<Directory>
+    {
+        private final Map<Class<? extends Directory>, Collection<Directory>> _map;
+
+        public DirectoryIterable(Map<Class<? extends Directory>, Collection<Directory>> map)
+        {
+            _map = map;
+        }
+
+        public Iterator<Directory> iterator()
+        {
+            return new DirectoryIterator(_map);
+        }
+
+        private static class DirectoryIterator implements Iterator<Directory>
+        {
+            @NotNull
+            private final Iterator<Map.Entry<Class<? extends Directory>, Collection<Directory>>> _mapIterator;
+            @Nullable
+            private Iterator<Directory> _listIterator;
+
+            public DirectoryIterator(Map<Class<? extends Directory>, Collection<Directory>> map)
+            {
+                _mapIterator = map.entrySet().iterator();
+
+                if (_mapIterator.hasNext())
+                    _listIterator = _mapIterator.next().getValue().iterator();
+            }
+
+            public boolean hasNext()
+            {
+                return _listIterator != null && (_listIterator.hasNext() || _mapIterator.hasNext());
+            }
+
+            public Directory next()
+            {
+                if (_listIterator == null || (!_listIterator.hasNext() && !_mapIterator.hasNext()))
+                    throw new NoSuchElementException();
+
+                while (!_listIterator.hasNext())
+                    _listIterator = _mapIterator.next().getValue().iterator();
+
+                return _listIterator.next();
+            }
+
+            public void remove()
+            {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+}
diff --git a/src/com/drew/metadata/MetadataException.java b/src/com/drew/metadata/MetadataException.java
new file mode 100644
index 0000000..72109de
--- /dev/null
+++ b/src/com/drew/metadata/MetadataException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.CompoundException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Base class for all metadata specific exceptions.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class MetadataException extends CompoundException
+{
+    private static final long serialVersionUID = 8612756143363919682L;
+
+    public MetadataException(@Nullable String msg)
+    {
+        super(msg);
+    }
+
+    public MetadataException(@Nullable Throwable exception)
+    {
+        super(exception);
+    }
+
+    public MetadataException(@Nullable String msg, @Nullable Throwable innerException)
+    {
+        super(msg, innerException);
+    }
+}
diff --git a/src/com/drew/metadata/MetadataReader.java b/src/com/drew/metadata/MetadataReader.java
new file mode 100644
index 0000000..5201149
--- /dev/null
+++ b/src/com/drew/metadata/MetadataReader.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Defines an object capable of processing a particular type of metadata from a {@link RandomAccessReader}.
+ * <p>
+ * Instances of this interface must be thread-safe and reusable.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public interface MetadataReader
+{
+    /**
+     * Extracts metadata from <code>reader</code> and merges it into the specified {@link Metadata} object.
+     *
+     * @param reader   The {@link RandomAccessReader} from which the metadata should be extracted.
+     * @param metadata The {@link Metadata} object into which extracted values should be merged.
+     */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata);
+}
diff --git a/src/com/drew/metadata/Tag.java b/src/com/drew/metadata/Tag.java
new file mode 100644
index 0000000..f8603ef
--- /dev/null
+++ b/src/com/drew/metadata/Tag.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Models a particular tag within a {@link com.drew.metadata.Directory} and provides methods for obtaining its value.
+ * Immutable.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class Tag
+{
+    private final int _tagType;
+    @NotNull
+    private final Directory _directory;
+
+    public Tag(int tagType, @NotNull Directory directory)
+    {
+        _tagType = tagType;
+        _directory = directory;
+    }
+
+    /**
+     * Gets the tag type as an int
+     *
+     * @return the tag type as an int
+     */
+    public int getTagType()
+    {
+        return _tagType;
+    }
+
+    /**
+     * Gets the tag type in hex notation as a String with padded leading
+     * zeroes if necessary (i.e. <code>0x100E</code>).
+     *
+     * @return the tag type as a string in hexadecimal notation
+     */
+    @NotNull
+    public String getTagTypeHex()
+    {
+        String hex = Integer.toHexString(_tagType);
+        while (hex.length() < 4) hex = "0" + hex;
+        return "0x" + hex;
+    }
+
+    /**
+     * Get a description of the tag's value, considering enumerated values
+     * and units.
+     *
+     * @return a description of the tag's value
+     */
+    @Nullable
+    public String getDescription()
+    {
+        return _directory.getDescription(_tagType);
+    }
+
+    /**
+     * Get whether this tag has a name.
+     *
+     * If <code>true</code>, it may be accessed via {@link #getTagName}.
+     * If <code>false</code>, {@link #getTagName} will return a string resembling <code>"Unknown tag (0x1234)"</code>.
+     *
+     * @return whether this tag has a name
+     */
+    @NotNull
+    public boolean hasTagName()
+    {
+        return _directory.hasTagName(_tagType);
+    }
+
+    /**
+     * Get the name of the tag, such as <code>Aperture</code>, or
+     * <code>InteropVersion</code>.
+     *
+     * @return the tag's name
+     */
+    @NotNull
+    public String getTagName()
+    {
+        return _directory.getTagName(_tagType);
+    }
+
+    /**
+     * Get the name of the {@link com.drew.metadata.Directory} in which the tag exists, such as
+     * <code>Exif</code>, <code>GPS</code> or <code>Interoperability</code>.
+     *
+     * @return name of the {@link com.drew.metadata.Directory} in which this tag exists
+     */
+    @NotNull
+    public String getDirectoryName()
+    {
+        return _directory.getName();
+    }
+
+    /**
+     * A basic representation of the tag's type and value.  EG: <code>[FNumber] F2.8</code>.
+     *
+     * @return the tag's type and value
+     */
+    @Override
+    @NotNull
+    public String toString()
+    {
+        String description = getDescription();
+        if (description == null)
+            description = _directory.getString(getTagType()) + " (unable to formulate description)";
+        return "[" + _directory.getName() + "] " + getTagName() + " - " + description;
+    }
+}
diff --git a/src/com/drew/metadata/TagDescriptor.java b/src/com/drew/metadata/TagDescriptor.java
new file mode 100644
index 0000000..50e7e46
--- /dev/null
+++ b/src/com/drew/metadata/TagDescriptor.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.Rational;
+import com.drew.lang.StringUtil;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Base class for all tag descriptor classes.  Implementations are responsible for
+ * providing the human-readable string representation of tag values stored in a directory.
+ * The directory is provided to the tag descriptor via its constructor.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class TagDescriptor<T extends Directory>
+{
+    @NotNull
+    protected final T _directory;
+
+    public TagDescriptor(@NotNull T directory)
+    {
+        _directory = directory;
+    }
+
+    /**
+     * Returns a descriptive value of the specified tag for this image.
+     * Where possible, known values will be substituted here in place of the raw
+     * tokens actually kept in the metadata segment.  If no substitution is
+     * available, the value provided by <code>getString(tagType)</code> will be returned.
+     *
+     * @param tagType the tag to find a description for
+     * @return a description of the image's value for the specified tag, or
+     *         <code>null</code> if the tag hasn't been defined.
+     */
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        Object object = _directory.getObject(tagType);
+
+        if (object == null)
+            return null;
+
+        // special presentation for long arrays
+        if (object.getClass().isArray()) {
+            final int length = Array.getLength(object);
+            if (length > 16) {
+                final String componentTypeName = object.getClass().getComponentType().getName();
+                return String.format("[%d %s%s]", length, componentTypeName, length == 1 ? "" : "s");
+            }
+        }
+
+        // no special handling required, so use default conversion to a string
+        return _directory.getString(tagType);
+    }
+
+    /**
+     * Takes a series of 4 bytes from the specified offset, and converts these to a
+     * well-known version number, where possible.
+     * <p>
+     * Two different formats are processed:
+     * <ul>
+     * <li>[30 32 31 30] -> 2.10</li>
+     * <li>[0 1 0 0] -> 1.00</li>
+     * </ul>
+     *
+     * @param components  the four version values
+     * @param majorDigits the number of components to be
+     * @return the version as a string of form "2.10" or null if the argument cannot be converted
+     */
+    @Nullable
+    public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits)
+    {
+        if (components == null)
+            return null;
+        StringBuilder version = new StringBuilder();
+        for (int i = 0; i < 4 && i < components.length; i++) {
+            if (i == majorDigits)
+                version.append('.');
+            char c = (char)components[i];
+            if (c < '0')
+                c += '0';
+            if (i == 0 && c == '0')
+                continue;
+            version.append(c);
+        }
+        return version.toString();
+    }
+
+    @Nullable
+    protected String getVersionBytesDescription(final int tagType, int majorDigits)
+    {
+        int[] values = _directory.getIntArray(tagType);
+        return values == null ? null : convertBytesToVersionString(values, majorDigits);
+    }
+
+    @Nullable
+    protected String getIndexedDescription(final int tagType, @NotNull String... descriptions)
+    {
+        return getIndexedDescription(tagType, 0, descriptions);
+    }
+
+    @Nullable
+    protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions)
+    {
+        final Integer index = _directory.getInteger(tagType);
+        if (index == null)
+            return null;
+        final int arrayIndex = index - baseIndex;
+        if (arrayIndex >= 0 && arrayIndex < descriptions.length) {
+            String description = descriptions[arrayIndex];
+            if (description != null)
+                return description;
+        }
+        return "Unknown (" + index + ")";
+    }
+
+    @Nullable
+    protected String getByteLengthDescription(final int tagType)
+    {
+        byte[] bytes = _directory.getByteArray(tagType);
+        if (bytes == null)
+            return null;
+        return String.format("(%d byte%s)", bytes.length, bytes.length == 1 ? "" : "s");
+    }
+
+    @Nullable
+    protected String getSimpleRational(final int tagType)
+    {
+        Rational value = _directory.getRational(tagType);
+        if (value == null)
+            return null;
+        return value.toSimpleString(true);
+    }
+
+    @Nullable
+    protected String getDecimalRational(final int tagType, final int decimalPlaces)
+    {
+        Rational value = _directory.getRational(tagType);
+        if (value == null)
+            return null;
+        return String.format("%." + decimalPlaces + "f", value.doubleValue());
+    }
+
+    @Nullable
+    protected String getFormattedInt(final int tagType, @NotNull final String format)
+    {
+        Integer value = _directory.getInteger(tagType);
+        if (value == null)
+            return null;
+        return String.format(format, value);
+    }
+
+    @Nullable
+    protected String getFormattedFloat(final int tagType, @NotNull final String format)
+    {
+        Float value = _directory.getFloatObject(tagType);
+        if (value == null)
+            return null;
+        return String.format(format, value);
+    }
+
+    @Nullable
+    protected String getFormattedString(final int tagType, @NotNull final String format)
+    {
+        String value = _directory.getString(tagType);
+        if (value == null)
+            return null;
+        return String.format(format, value);
+    }
+
+    @Nullable
+    protected String getEpochTimeDescription(final int tagType)
+    {
+        // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
+        Long value = _directory.getLongObject(tagType);
+        if (value==null)
+            return null;
+        return new Date(value).toString();
+    }
+
+    /**
+     * LSB first. Labels may be null, a String, or a String[2] with (low label,high label) values.
+     */
+    @Nullable
+    protected String getBitFlagDescription(final int tagType, @NotNull final Object... labels)
+    {
+        Integer value = _directory.getInteger(tagType);
+
+        if (value == null)
+            return null;
+
+        List<String> parts = new ArrayList<String>();
+
+        int bitIndex = 0;
+        while (labels.length > bitIndex) {
+            Object labelObj = labels[bitIndex];
+            if (labelObj != null) {
+                boolean isBitSet = (value & 1) == 1;
+                if (labelObj instanceof String[]) {
+                    String[] labelPair = (String[])labelObj;
+                    assert(labelPair.length == 2);
+                    parts.add(labelPair[isBitSet ? 1 : 0]);
+                } else if (isBitSet && labelObj instanceof String) {
+                    parts.add((String)labelObj);
+                }
+            }
+            value >>= 1;
+            bitIndex++;
+        }
+
+        return StringUtil.join(parts, ", ");
+    }
+
+    @Nullable
+    protected String get7BitStringFromBytes(final int tagType)
+    {
+        final byte[] bytes = _directory.getByteArray(tagType);
+
+        if (bytes == null)
+            return null;
+
+        int length = bytes.length;
+        for (int index = 0; index < bytes.length; index++) {
+            int i = bytes[index] & 0xFF;
+            if (i == 0 || i > 0x7F) {
+                length = index;
+                break;
+            }
+        }
+
+        return new String(bytes, 0, length);
+    }
+
+    @Nullable
+    protected String getAsciiStringFromBytes(int tag)
+    {
+        byte[] values = _directory.getByteArray(tag);
+
+        if (values == null)
+            return null;
+
+        try {
+            return new String(values, "ASCII").trim();
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifDescriptorBase.java b/src/com/drew/metadata/exif/ExifDescriptorBase.java
new file mode 100644
index 0000000..7f32352
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifDescriptorBase.java
@@ -0,0 +1,1105 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.imaging.PhotographicConversions;
+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.TagDescriptor;
+
+import java.io.UnsupportedEncodingException;
+import java.text.DecimalFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.drew.metadata.exif.ExifDirectoryBase.*;
+
+/**
+ * Base class for several Exif format descriptor classes.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class ExifDescriptorBase<T extends Directory> extends TagDescriptor<T>
+{
+    /**
+     * Dictates whether rational values will be represented in decimal format in instances
+     * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3).
+     */
+    private final boolean _allowDecimalRepresentationOfRationals = true;
+
+    @NotNull
+    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
+    @NotNull
+    private static final java.text.DecimalFormat SimpleDecimalFormatterWithPrecision = new DecimalFormat("0.0");
+
+    // Note for the potential addition of brightness presentation in eV:
+    // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv),
+    // you must add SensitivityValue(Sv).
+    // Ev=BV+Sv   Sv=log2(ISOSpeedRating/3.125)
+    // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32.
+
+    public ExifDescriptorBase(@NotNull T directory)
+    {
+        super(directory);
+    }
+
+    @Nullable
+    @Override
+    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:
+                return getInteropIndexDescription();
+            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_IMAGE_WIDTH:
+                return getImageWidthDescription();
+            case TAG_IMAGE_HEIGHT:
+                return getImageHeightDescription();
+            case TAG_BITS_PER_SAMPLE:
+                return getBitsPerSampleDescription();
+            case TAG_PHOTOMETRIC_INTERPRETATION:
+                return getPhotometricInterpretationDescription();
+            case TAG_ROWS_PER_STRIP:
+                return getRowsPerStripDescription();
+            case TAG_STRIP_BYTE_COUNTS:
+                return getStripByteCountsDescription();
+            case TAG_SAMPLES_PER_PIXEL:
+                return getSamplesPerPixelDescription();
+            case TAG_PLANAR_CONFIGURATION:
+                return getPlanarConfigurationDescription();
+            case TAG_YCBCR_SUBSAMPLING:
+                return getYCbCrSubsamplingDescription();
+            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_EXPOSURE_TIME:
+                return getExposureTimeDescription();
+            case TAG_SHUTTER_SPEED:
+                return getShutterSpeedDescription();
+            case TAG_FNUMBER:
+                return getFNumberDescription();
+            case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
+                return getCompressedAverageBitsPerPixelDescription();
+            case TAG_SUBJECT_DISTANCE:
+                return getSubjectDistanceDescription();
+            case TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FLASH:
+                return getFlashDescription();
+            case TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case TAG_COLOR_SPACE:
+                return getColorSpaceDescription();
+            case TAG_EXIF_IMAGE_WIDTH:
+                return getExifImageWidthDescription();
+            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_SENSING_METHOD:
+                return getSensingMethodDescription();
+            case TAG_EXPOSURE_BIAS:
+                return getExposureBiasDescription();
+            case TAG_FILE_SOURCE:
+                return getFileSourceDescription();
+            case TAG_SCENE_TYPE:
+                return getSceneTypeDescription();
+            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();
+            case TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case TAG_WHITE_BALANCE_MODE:
+                return getWhiteBalanceModeDescription();
+            case TAG_DIGITAL_ZOOM_RATIO:
+                return getDigitalZoomRatioDescription();
+            case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
+                return get35mmFilmEquivFocalLengthDescription();
+            case TAG_SCENE_CAPTURE_TYPE:
+                return getSceneCaptureTypeDescription();
+            case TAG_GAIN_CONTROL:
+                return getGainControlDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_SUBJECT_DISTANCE_RANGE:
+                return getSubjectDistanceRangeDescription();
+            case TAG_SENSITIVITY_TYPE:
+                return getSensitivityTypeRangeDescription();
+            case TAG_COMPRESSION:
+                return getCompressionDescription();
+            case TAG_JPEG_PROC:
+                return getJpegProcDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getInteropVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_INTEROP_VERSION, 2);
+    }
+
+    @Nullable
+    public String getInteropIndexDescription()
+    {
+        String value = _directory.getString(TAG_INTEROP_INDEX);
+
+        if (value == null)
+            return null;
+
+        return "R98".equalsIgnoreCase(value.trim())
+            ? "Recommended Exif Interoperability Rules (ExifR98)"
+            : "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getReferenceBlackWhiteDescription()
+    {
+        int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
+        if (ints==null || ints.length < 6)
+            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 getIndexedDescription(TAG_ORIENTATION, 1,
+            "Top, left side (Horizontal / normal)",
+            "Top, right side (Mirror horizontal)",
+            "Bottom, right side (Rotate 180)",
+            "Bottom, left side (Mirror vertical)",
+            "Left side, top (Mirror horizontal and rotate 270 CW)",
+            "Right side, top (Rotate 90 CW)",
+            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
+            "Left side, bottom (Rotate 270 CW)");
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
+    }
+
+    /** The Windows specific tags uses plain Unicode. */
+    @Nullable
+    private String getUnicodeDescription(int tag)
+    {
+        byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null)
+            return null;
+        try {
+            // Decode the unicode string and trim the unicode zero "\0" from the end.
+            return new String(bytes, "UTF-16LE").trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String 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";
+    }
+
+    @Nullable
+    public String getNewSubfileTypeDescription()
+    {
+        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page reduced-resolution image",
+            "Transparency mask",
+            "Transparency mask of reduced-resolution image",
+            "Transparency mask of multi-page image",
+            "Transparency mask of reduced-resolution multi-page image"
+        );
+    }
+
+    @Nullable
+    public String getSubfileTypeDescription()
+    {
+        return getIndexedDescription(TAG_SUBFILE_TYPE, 1,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page image"
+        );
+    }
+
+    @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 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"
+            : SimpleDecimalFormatter.format(value) + "mm";
+    }
+
+    @Nullable
+    public String getDigitalZoomRatioDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+            ? "Digital zoom not used."
+            : SimpleDecimalFormatter.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"
+        );
+    }
+
+    @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 "f/" + SimpleDecimalFormatterWithPrecision.format(fStop);
+    }
+
+    @Nullable
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return "f/" + SimpleDecimalFormatterWithPrecision.format(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);
+        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 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);
+        if (value == null)
+            return null;
+        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " mm";
+    }
+
+    @Nullable
+    public String getFlashDescription()
+    {
+        /*
+         * This is a 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()
+    {
+        // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '4' flash,
+        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
+        // '21' D65, '22' D75, '255' other.
+        // see http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
+        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 Flourescent";
+            case 13: return "Day White Flourescent";
+            case 14: return "Cool White Flourescent";
+            case 15: return "White Flourescent";
+            case 16: return "Warm White Flourescent";
+            case 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 + ")";
+        }
+    }
+
+    @Nullable
+    public String getCompressionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_COMPRESSION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Uncompressed";
+            case 2: return "CCITT 1D";
+            case 3: return "T4/Group 3 Fax";
+            case 4: return "T6/Group 4 Fax";
+            case 5: return "LZW";
+            case 6: return "JPEG (old-style)";
+            case 7: return "JPEG";
+            case 8: return "Adobe Deflate";
+            case 9: return "JBIG B&W";
+            case 10: return "JBIG Color";
+            case 99: return "JPEG";
+            case 262: return "Kodak 262";
+            case 32766: return "Next";
+            case 32767: return "Sony ARW Compressed";
+            case 32769: return "Packed RAW";
+            case 32770: return "Samsung SRW Compressed";
+            case 32771: return "CCIRLEW";
+            case 32772: return "Samsung SRW Compressed 2";
+            case 32773: return "PackBits";
+            case 32809: return "Thunderscan";
+            case 32867: return "Kodak KDC Compressed";
+            case 32895: return "IT8CTPAD";
+            case 32896: return "IT8LW";
+            case 32897: return "IT8MP";
+            case 32898: return "IT8BL";
+            case 32908: return "PixarFilm";
+            case 32909: return "PixarLog";
+            case 32946: return "Deflate";
+            case 32947: return "DCS";
+            case 34661: return "JBIG";
+            case 34676: return "SGILog";
+            case 34677: return "SGILog24";
+            case 34712: return "JPEG 2000";
+            case 34713: return "Nikon NEF Compressed";
+            case 34715: return "JBIG2 TIFF FX";
+            case 34718: return "Microsoft Document Imaging (MDI) Binary Level Codec";
+            case 34719: return "Microsoft Document Imaging (MDI) Progressive Transform Codec";
+            case 34720: return "Microsoft Document Imaging (MDI) Vector";
+            case 34892: return "Lossy JPEG";
+            case 65000: return "Kodak DCR Compressed";
+            case 65535: return "Pentax PEF Compressed";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSubjectDistanceDescription()
+    {
+        Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE);
+        if (value == null)
+            return null;
+        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " metres";
+    }
+
+    @Nullable
+    public String getCompressedAverageBitsPerPixelDescription()
+    {
+        Rational value = _directory.getRational(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);
+        if (value == null)
+            return null;
+        String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals);
+        return value.isInteger() && value.intValue() == 1
+            ? ratio + " bit/pixel"
+            : ratio + " bits/pixel";
+    }
+
+    @Nullable
+    public String getExposureTimeDescription()
+    {
+        String value = _directory.getString(TAG_EXPOSURE_TIME);
+        return value == null ? null : value + " sec";
+    }
+
+    @Nullable
+    public String getShutterSpeedDescription()
+    {
+        // I believe this method to now be stable, but am leaving some alternative snippets of
+        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
+
+//        float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+//        int apexPower = (int)Math.pow(2.0, apexValue);
+//        return "1/" + apexPower + " sec";
+        // TODO test this method
+        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
+        // description (spotted bug using a Canon EOS 300D)
+        // thanks also to Gli Blr for spotting this bug
+        Float apexValue = _directory.getFloatObject(TAG_SHUTTER_SPEED);
+        if (apexValue == null)
+            return null;
+        if (apexValue <= 1) {
+            float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
+            long apexPower10 = Math.round((double)apexPower * 10.0);
+            float fApexPower = (float)apexPower10 / 10.0f;
+            return fApexPower + " sec";
+        } else {
+            int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
+            return "1/" + apexPower + " sec";
+        }
+
+/*
+        // This alternative implementation offered by Bill Richards
+        // TODO determine which is the correct / more-correct implementation
+        double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+        double apexPower = Math.pow(2.0, apexValue);
+
+        StringBuffer sb = new StringBuffer();
+        if (apexPower > 1)
+            apexPower = Math.floor(apexPower);
+
+        if (apexPower < 1) {
+            sb.append((int)Math.round(1/apexPower));
+        } else {
+            sb.append("1/");
+            sb.append((int)apexPower);
+        }
+        sb.append(" sec");
+        return sb.toString();
+*/
+    }
+
+    @Nullable
+    public String getFNumberDescription()
+    {
+        Rational value = _directory.getRational(TAG_FNUMBER);
+        if (value == null)
+            return null;
+        return "f/" + SimpleDecimalFormatterWithPrecision.format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getSensingMethodDescription()
+    {
+        // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor
+        // '4' Three-chip color area sensor, '5' Color sequential area sensor
+        // '7' Trilinear sensor '8' Color sequential linear sensor,  'Other' reserved
+        return getIndexedDescription(TAG_SENSING_METHOD,
+            1,
+            "(Not defined)",
+            "One-chip color area sensor",
+            "Two-chip color area sensor",
+            "Three-chip color area sensor",
+            "Color sequential area sensor",
+            null,
+            "Trilinear sensor",
+            "Color sequential linear sensor"
+        );
+    }
+
+    @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();
+    }
+
+    @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 + ")";
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifDirectoryBase.java b/src/com/drew/metadata/exif/ExifDirectoryBase.java
new file mode 100644
index 0000000..5e94f8e
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifDirectoryBase.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Base class for several Exif format tag directories.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class ExifDirectoryBase extends Directory
+{
+    public static final int TAG_INTEROP_INDEX = 0x0001;
+    public static final int TAG_INTEROP_VERSION = 0x0002;
+
+    /**
+     * The new subfile type tag.
+     * 0 = Full-resolution Image
+     * 1 = Reduced-resolution image
+     * 2 = Single page of multi-page image
+     * 3 = Single page of multi-page reduced-resolution image
+     * 4 = Transparency mask
+     * 5 = Transparency mask of reduced-resolution image
+     * 6 = Transparency mask of multi-page image
+     * 7 = Transparency mask of reduced-resolution multi-page image
+     */
+    public static final int TAG_NEW_SUBFILE_TYPE                  = 0x00FE;
+    /**
+     * The old subfile type tag.
+     * 1 = Full-resolution image (Main image)
+     * 2 = Reduced-resolution image (Thumbnail)
+     * 3 = Single page of multi-page image
+     */
+    public static final int TAG_SUBFILE_TYPE                      = 0x00FF;
+
+    public static final int TAG_IMAGE_WIDTH                       = 0x0100;
+    public static final int TAG_IMAGE_HEIGHT                      = 0x0101;
+
+    /**
+     * When image format is no compression, this value shows the number of bits
+     * per component for each pixel. Usually this value is '8,8,8'.
+     */
+    public static final int TAG_BITS_PER_SAMPLE                   = 0x0102;
+    public static final int TAG_COMPRESSION                       = 0x0103;
+
+    /**
+     * Shows the color space of the image data components.
+     * 0 = WhiteIsZero
+     * 1 = BlackIsZero
+     * 2 = RGB
+     * 3 = RGB Palette
+     * 4 = Transparency Mask
+     * 5 = CMYK
+     * 6 = YCbCr
+     * 8 = CIELab
+     * 9 = ICCLab
+     * 10 = ITULab
+     * 32803 = Color Filter Array
+     * 32844 = Pixar LogL
+     * 32845 = Pixar LogLuv
+     * 34892 = Linear Raw
+     */
+    public static final int TAG_PHOTOMETRIC_INTERPRETATION        = 0x0106;
+
+    /**
+     * 1 = No dithering or halftoning
+     * 2 = Ordered dither or halftone
+     * 3 = Randomized dither
+     */
+    public static final int TAG_THRESHOLDING                      = 0x0107;
+
+    /**
+     * 1 = Normal
+     * 2 = Reversed
+     */
+    public static final int TAG_FILL_ORDER                        = 0x010A;
+    public static final int TAG_DOCUMENT_NAME                     = 0x010D;
+
+    public static final int TAG_IMAGE_DESCRIPTION                 = 0x010E;
+
+    public static final int TAG_MAKE                              = 0x010F;
+    public static final int TAG_MODEL                             = 0x0110;
+    /** The position in the file of raster data. */
+    public static final int TAG_STRIP_OFFSETS                     = 0x0111;
+    public static final int TAG_ORIENTATION                       = 0x0112;
+    /** Each pixel is composed of this many samples. */
+    public static final int TAG_SAMPLES_PER_PIXEL                 = 0x0115;
+    /** The raster is codified by a single block of data holding this many rows. */
+    public static final int TAG_ROWS_PER_STRIP                    = 0x0116;
+    /** The size of the raster data in bytes. */
+    public static final int TAG_STRIP_BYTE_COUNTS                 = 0x0117;
+    public static final int TAG_MIN_SAMPLE_VALUE                  = 0x0118;
+    public static final int TAG_MAX_SAMPLE_VALUE                  = 0x0119;
+    public static final int TAG_X_RESOLUTION                      = 0x011A;
+    public static final int TAG_Y_RESOLUTION                      = 0x011B;
+    /**
+     * When image format is no compression YCbCr, this value shows byte aligns of
+     * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for
+     * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and
+     * stored to Y plane/Cb plane/Cr plane format.
+     */
+    public static final int TAG_PLANAR_CONFIGURATION              = 0x011C;
+    public static final int TAG_PAGE_NAME                         = 0x011D;
+
+    public static final int TAG_RESOLUTION_UNIT                   = 0x0128;
+    public static final int TAG_TRANSFER_FUNCTION                 = 0x012D;
+    public static final int TAG_SOFTWARE                          = 0x0131;
+    public static final int TAG_DATETIME                          = 0x0132;
+    public static final int TAG_ARTIST                            = 0x013B;
+    public static final int TAG_HOST_COMPUTER                     = 0x013C;
+    public static final int TAG_PREDICTOR                         = 0x013D;
+    public static final int TAG_WHITE_POINT                       = 0x013E;
+    public static final int TAG_PRIMARY_CHROMATICITIES            = 0x013F;
+
+    public static final int TAG_TILE_WIDTH                        = 0x0142;
+    public static final int TAG_TILE_LENGTH                       = 0x0143;
+    public static final int TAG_TILE_OFFSETS                      = 0x0144;
+    public static final int TAG_TILE_BYTE_COUNTS                  = 0x0145;
+
+    public static final int TAG_SUB_IFD_OFFSET                    = 0x014a;
+
+    public static final int TAG_TRANSFER_RANGE                    = 0x0156;
+    public static final int TAG_JPEG_TABLES                       = 0x015B;
+    public static final int TAG_JPEG_PROC                         = 0x0200;
+
+    public static final int TAG_YCBCR_COEFFICIENTS                = 0x0211;
+    public static final int TAG_YCBCR_SUBSAMPLING                 = 0x0212;
+    public static final int TAG_YCBCR_POSITIONING                 = 0x0213;
+    public static final int TAG_REFERENCE_BLACK_WHITE             = 0x0214;
+
+    public static final int TAG_RELATED_IMAGE_FILE_FORMAT         = 0x1000;
+    public static final int TAG_RELATED_IMAGE_WIDTH               = 0x1001;
+    public static final int TAG_RELATED_IMAGE_HEIGHT              = 0x1002;
+
+    public static final int TAG_RATING                            = 0x4746;
+
+    public static final int TAG_CFA_REPEAT_PATTERN_DIM            = 0x828D;
+    /** There are two definitions for CFA pattern, I don't know the difference... */
+    public static final int TAG_CFA_PATTERN_2                     = 0x828E;
+    public static final int TAG_BATTERY_LEVEL                     = 0x828F;
+    public static final int TAG_COPYRIGHT                         = 0x8298;
+    /**
+     * Exposure time (reciprocal of shutter speed). Unit is second.
+     */
+    public static final int TAG_EXPOSURE_TIME                     = 0x829A;
+    /**
+     * The actual F-number(F-stop) of lens when the image was taken.
+     */
+    public static final int TAG_FNUMBER                           = 0x829D;
+    public static final int TAG_IPTC_NAA                          = 0x83BB;
+    public static final int TAG_INTER_COLOR_PROFILE               = 0x8773;
+    /**
+     * Exposure program that the camera used when image was taken. '1' means
+     * manual control, '2' program normal, '3' aperture priority, '4' shutter
+     * priority, '5' program creative (slow program), '6' program action
+     * (high-speed program), '7' portrait mode, '8' landscape mode.
+     */
+    public static final int TAG_EXPOSURE_PROGRAM                  = 0x8822;
+    public static final int TAG_SPECTRAL_SENSITIVITY              = 0x8824;
+    public static final int TAG_ISO_EQUIVALENT                    = 0x8827;
+    /**
+     * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.
+     * <p>
+     * OECF is the relationship between the camera optical input and the image values.
+     * <p>
+     * The values are:
+     * <ul>
+     *   <li>Two shorts, indicating respectively number of columns, and number of rows.</li>
+     *   <li>For each column, the column name in a null-terminated ASCII string.</li>
+     *   <li>For each cell, an SRATIONAL value.</li>
+     * </ul>
+     */
+    public static final int TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION = 0x8828;
+    public static final int TAG_INTERLACE                         = 0x8829;
+    public static final int TAG_TIME_ZONE_OFFSET_TIFF_EP          = 0x882A;
+    public static final int TAG_SELF_TIMER_MODE_TIFF_EP           = 0x882B;
+    /**
+     * Applies to ISO tag.
+     *
+     * 0 = Unknown
+     * 1 = Standard Output Sensitivity
+     * 2 = Recommended Exposure Index
+     * 3 = ISO Speed
+     * 4 = Standard Output Sensitivity and Recommended Exposure Index
+     * 5 = Standard Output Sensitivity and ISO Speed
+     * 6 = Recommended Exposure Index and ISO Speed
+     * 7 = Standard Output Sensitivity, Recommended Exposure Index and ISO Speed
+     */
+    public static final int TAG_SENSITIVITY_TYPE                  = 0x8830;
+    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_EXIF_VERSION                      = 0x9000;
+    public static final int TAG_DATETIME_ORIGINAL                 = 0x9003;
+    public static final int TAG_DATETIME_DIGITIZED                = 0x9004;
+
+    public static final int TAG_COMPONENTS_CONFIGURATION          = 0x9101;
+    /**
+     * Average (rough estimate) compression level in JPEG bits per pixel.
+     * */
+    public static final int TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL = 0x9102;
+
+    /**
+     * Shutter speed by APEX value. To convert this value to ordinary 'Shutter Speed';
+     * calculate this value's power of 2, then reciprocal. For example, if the
+     * ShutterSpeedValue is '4', shutter speed is 1/(24)=1/16 second.
+     */
+    public static final int TAG_SHUTTER_SPEED                     = 0x9201;
+    /**
+     * The actual aperture value of lens when the image was taken. Unit is APEX.
+     * To convert this value to ordinary F-number (F-stop), calculate this value's
+     * power of root 2 (=1.4142). For example, if the ApertureValue is '5',
+     * F-number is 1.4142^5 = F5.6.
+     */
+    public static final int TAG_APERTURE                          = 0x9202;
+    public static final int TAG_BRIGHTNESS_VALUE                  = 0x9203;
+    public static final int TAG_EXPOSURE_BIAS                     = 0x9204;
+    /**
+     * Maximum aperture value of lens. You can convert to F-number by calculating
+     * power of root 2 (same process of ApertureValue:0x9202).
+     * The actual aperture value of lens when the image was taken. To convert this
+     * value to ordinary f-number(f-stop), calculate the value's power of root 2
+     * (=1.4142). For example, if the ApertureValue is '5', f-number is 1.41425^5 = F5.6.
+     */
+    public static final int TAG_MAX_APERTURE                      = 0x9205;
+    /**
+     * Indicates the distance the autofocus camera is focused to.  Tends to be less accurate as distance increases.
+     */
+    public static final int TAG_SUBJECT_DISTANCE                  = 0x9206;
+    /**
+     * Exposure metering method. '0' means unknown, '1' average, '2' center
+     * weighted average, '3' spot, '4' multi-spot, '5' multi-segment, '6' partial,
+     * '255' other.
+     */
+    public static final int TAG_METERING_MODE                     = 0x9207;
+
+    public static final int TAG_LIGHT_SOURCE                      = 0x9208; // TODO duplicate tag
+    /**
+     * White balance (aka light source). '0' means unknown, '1' daylight,
+     * '2' fluorescent, '3' tungsten, '10' flash, '17' standard light A,
+     * '18' standard light B, '19' standard light C, '20' D55, '21' D65,
+     * '22' D75, '255' other.
+     */
+    public static final int TAG_WHITE_BALANCE                     = 0x9208; // TODO duplicate tag
+    /**
+     * 0x0  = 0000000 = No Flash
+     * 0x1  = 0000001 = Fired
+     * 0x5  = 0000101 = Fired, Return not detected
+     * 0x7  = 0000111 = Fired, Return detected
+     * 0x9  = 0001001 = On
+     * 0xd  = 0001101 = On, Return not detected
+     * 0xf  = 0001111 = On, Return detected
+     * 0x10 = 0010000 = Off
+     * 0x18 = 0011000 = Auto, Did not fire
+     * 0x19 = 0011001 = Auto, Fired
+     * 0x1d = 0011101 = Auto, Fired, Return not detected
+     * 0x1f = 0011111 = Auto, Fired, Return detected
+     * 0x20 = 0100000 = No flash function
+     * 0x41 = 1000001 = Fired, Red-eye reduction
+     * 0x45 = 1000101 = Fired, Red-eye reduction, Return not detected
+     * 0x47 = 1000111 = Fired, Red-eye reduction, Return detected
+     * 0x49 = 1001001 = On, Red-eye reduction
+     * 0x4d = 1001101 = On, Red-eye reduction, Return not detected
+     * 0x4f = 1001111 = On, Red-eye reduction, Return detected
+     * 0x59 = 1011001 = Auto, Fired, Red-eye reduction
+     * 0x5d = 1011101 = Auto, Fired, Red-eye reduction, Return not detected
+     * 0x5f = 1011111 = Auto, Fired, Red-eye reduction, Return detected
+     *        6543210 (positions)
+     *
+     * This is a bitmask.
+     * 0 = flash fired
+     * 1 = return detected
+     * 2 = return able to be detected
+     * 3 = unknown
+     * 4 = auto used
+     * 5 = unknown
+     * 6 = red eye reduction used
+     */
+    public static final int TAG_FLASH                             = 0x9209;
+    /**
+     * Focal length of lens used to take image.  Unit is millimeter.
+     * Nice digital cameras actually save the focal length as a function of how far they are zoomed in.
+     */
+    public static final int TAG_FOCAL_LENGTH                      = 0x920A;
+
+    public static final int TAG_FLASH_ENERGY_TIFF_EP              = 0x920B;
+    public static final int TAG_SPATIAL_FREQ_RESPONSE_TIFF_EP     = 0x920C;
+    public static final int TAG_NOISE                             = 0x920D;
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION_TIFF_EP  = 0x920E;
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION_TIFF_EP = 0x920F;
+    public static final int TAG_IMAGE_NUMBER                      = 0x9211;
+    public static final int TAG_SECURITY_CLASSIFICATION           = 0x9212;
+    public static final int TAG_IMAGE_HISTORY                     = 0x9213;
+    public static final int TAG_SUBJECT_LOCATION_TIFF_EP          = 0x9214;
+    public static final int TAG_EXPOSURE_INDEX_TIFF_EP            = 0x9215;
+    public static final int TAG_STANDARD_ID_TIFF_EP               = 0x9216;
+
+    /**
+     * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs.
+     * To determine the format, we consider the starting bytes of the makernote itself and sometimes the
+     * camera model and make.
+     * <p>
+     * The component count for this tag includes all of the bytes needed for the makernote.
+     */
+    public static final int TAG_MAKERNOTE                         = 0x927C;
+
+    public static final int TAG_USER_COMMENT                      = 0x9286;
+
+    public static final int TAG_SUBSECOND_TIME                    = 0x9290;
+    public static final int TAG_SUBSECOND_TIME_ORIGINAL           = 0x9291;
+    public static final int TAG_SUBSECOND_TIME_DIGITIZED          = 0x9292;
+
+    /** The image title, as used by Windows XP. */
+    public static final int TAG_WIN_TITLE                         = 0x9C9B;
+    /** The image comment, as used by Windows XP. */
+    public static final int TAG_WIN_COMMENT                       = 0x9C9C;
+    /** The image author, as used by Windows XP (called Artist in the Windows shell). */
+    public static final int TAG_WIN_AUTHOR                        = 0x9C9D;
+    /** The image keywords, as used by Windows XP. */
+    public static final int TAG_WIN_KEYWORDS                      = 0x9C9E;
+    /** The image subject, as used by Windows XP. */
+    public static final int TAG_WIN_SUBJECT                       = 0x9C9F;
+
+    public static final int TAG_FLASHPIX_VERSION                  = 0xA000;
+    /**
+     * Defines Color Space. DCF image must use sRGB color space so value is
+     * always '1'. If the picture uses the other color space, value is
+     * '65535':Uncalibrated.
+     */
+    public static final int TAG_COLOR_SPACE                       = 0xA001;
+    public static final int TAG_EXIF_IMAGE_WIDTH                  = 0xA002;
+    public static final int TAG_EXIF_IMAGE_HEIGHT                 = 0xA003;
+    public static final int TAG_RELATED_SOUND_FILE                = 0xA004;
+
+    public static final int TAG_FLASH_ENERGY                      = 0xA20B;
+    public static final int TAG_SPATIAL_FREQ_RESPONSE             = 0xA20C;
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION          = 0xA20E;
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION          = 0xA20F;
+    /**
+     * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
+     * '2' inch, '3' centimeter.
+     *
+     * Note: Some of Fujifilm's digicam(e.g.FX2700,FX2900,Finepix4700Z/40i etc)
+     * uses value '3' so it must be 'centimeter', but it seems that they use a
+     * '8.3mm?'(1/3in.?) to their ResolutionUnit. Fuji's BUG? Finepix4900Z has
+     * been changed to use value '2' but it doesn't match to actual value also.
+     */
+    public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT       = 0xA210;
+    public static final int TAG_SUBJECT_LOCATION                  = 0xA214;
+    public static final int TAG_EXPOSURE_INDEX                    = 0xA215;
+    public static final int TAG_SENSING_METHOD                    = 0xA217;
+
+    public static final int TAG_FILE_SOURCE                       = 0xA300;
+    public static final int TAG_SCENE_TYPE                        = 0xA301;
+    public static final int TAG_CFA_PATTERN                       = 0xA302;
+
+    /**
+     * This tag indicates the use of special processing on image data, such as rendering
+     * geared to output. When special processing is performed, the reader is expected to
+     * disable or minimize any further processing.
+     * Tag = 41985 (A401.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal process
+     *   1 = Custom process
+     *   Other = reserved
+     */
+    public static final int TAG_CUSTOM_RENDERED                   = 0xA401;
+    /**
+     * This tag indicates the exposure mode set when the image was shot. In auto-bracketing
+     * mode, the camera shoots a series of frames of the same scene at different exposure settings.
+     * Tag = 41986 (A402.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = Auto exposure
+     *   1 = Manual exposure
+     *   2 = Auto bracket
+     *   Other = reserved
+     */
+    public static final int TAG_EXPOSURE_MODE                     = 0xA402;
+    /**
+     * This tag indicates the white balance mode set when the image was shot.
+     * Tag = 41987 (A403.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = Auto white balance
+     *   1 = Manual white balance
+     *   Other = reserved
+     */
+    public static final int TAG_WHITE_BALANCE_MODE                = 0xA403;
+    /**
+     * This tag indicates the digital zoom ratio when the image was shot. If the
+     * numerator of the recorded value is 0, this indicates that digital zoom was
+     * not used.
+     * Tag = 41988 (A404.H)
+     * Type = RATIONAL
+     * Count = 1
+     * Default = none
+     */
+    public static final int TAG_DIGITAL_ZOOM_RATIO                = 0xA404;
+    /**
+     * This tag indicates the equivalent focal length assuming a 35mm film camera,
+     * in mm. A value of 0 means the focal length is unknown. Note that this tag
+     * differs from the FocalLength tag.
+     * Tag = 41989 (A405.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     */
+    public static final int TAG_35MM_FILM_EQUIV_FOCAL_LENGTH      = 0xA405;
+    /**
+     * This tag indicates the type of scene that was shot. It can also be used to
+     * record the mode in which the image was shot. Note that this differs from
+     * the scene type (SceneType) tag.
+     * Tag = 41990 (A406.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Standard
+     *   1 = Landscape
+     *   2 = Portrait
+     *   3 = Night scene
+     *   Other = reserved
+     */
+    public static final int TAG_SCENE_CAPTURE_TYPE                = 0xA406;
+    /**
+     * This tag indicates the degree of overall image gain adjustment.
+     * Tag = 41991 (A407.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = None
+     *   1 = Low gain up
+     *   2 = High gain up
+     *   3 = Low gain down
+     *   4 = High gain down
+     *   Other = reserved
+     */
+    public static final int TAG_GAIN_CONTROL                      = 0xA407;
+    /**
+     * This tag indicates the direction of contrast processing applied by the camera
+     * when the image was shot.
+     * Tag = 41992 (A408.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Soft
+     *   2 = Hard
+     *   Other = reserved
+     */
+    public static final int TAG_CONTRAST                          = 0xA408;
+    /**
+     * This tag indicates the direction of saturation processing applied by the camera
+     * when the image was shot.
+     * Tag = 41993 (A409.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Low saturation
+     *   2 = High saturation
+     *   Other = reserved
+     */
+    public static final int TAG_SATURATION                        = 0xA409;
+    /**
+     * This tag indicates the direction of sharpness processing applied by the camera
+     * when the image was shot.
+     * Tag = 41994 (A40A.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Soft
+     *   2 = Hard
+     *   Other = reserved
+     */
+    public static final int TAG_SHARPNESS                         = 0xA40A;
+    /**
+     * This tag indicates information on the picture-taking conditions of a particular
+     * camera model. The tag is used only to indicate the picture-taking conditions in
+     * the reader.
+     * Tag = 41995 (A40B.H)
+     * Type = UNDEFINED
+     * Count = Any
+     * Default = none
+     *
+     * The information is recorded in the format shown below. The data is recorded
+     * in Unicode using SHORT type for the number of display rows and columns and
+     * UNDEFINED type for the camera settings. The Unicode (UCS-2) string including
+     * Signature is NULL terminated. The specifics of the Unicode string are as given
+     * in ISO/IEC 10464-1.
+     *
+     *      Length  Type        Meaning
+     *      ------+-----------+------------------
+     *      2       SHORT       Display columns
+     *      2       SHORT       Display rows
+     *      Any     UNDEFINED   Camera setting-1
+     *      Any     UNDEFINED   Camera setting-2
+     *      :       :           :
+     *      Any     UNDEFINED   Camera setting-n
+     */
+    public static final int TAG_DEVICE_SETTING_DESCRIPTION        = 0xA40B;
+    /**
+     * This tag indicates the distance to the subject.
+     * Tag = 41996 (A40C.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = unknown
+     *   1 = Macro
+     *   2 = Close view
+     *   3 = Distant view
+     *   Other = reserved
+     */
+    public static final int TAG_SUBJECT_DISTANCE_RANGE            = 0xA40C;
+
+    /**
+     * This tag indicates an identifier assigned uniquely to each image. It is
+     * recorded as an ASCII string equivalent to hexadecimal notation and 128-bit
+     * fixed length.
+     * Tag = 42016 (A420.H)
+     * Type = ASCII
+     * Count = 33
+     * Default = none
+     */
+    public static final int TAG_IMAGE_UNIQUE_ID                   = 0xA420;
+    /** String. */
+    public static final int TAG_CAMERA_OWNER_NAME                 = 0xA430;
+    /** String. */
+    public static final int TAG_BODY_SERIAL_NUMBER                = 0xA431;
+    /** An array of four Rational64u numbers giving focal and aperture ranges. */
+    public static final int TAG_LENS_SPECIFICATION                = 0xA432;
+    /** String. */
+    public static final int TAG_LENS_MAKE                         = 0xA433;
+    /** String. */
+    public static final int TAG_LENS_MODEL                        = 0xA434;
+    /** String. */
+    public static final int TAG_LENS_SERIAL_NUMBER                = 0xA435;
+    /** Rational64u. */
+    public static final int TAG_GAMMA                             = 0xA500;
+
+    public static final int TAG_PRINT_IM                          = 0xC4A5;
+
+    public static final int TAG_PANASONIC_TITLE                   = 0xC6D2;
+    public static final int TAG_PANASONIC_TITLE_2                 = 0xC6D3;
+
+    public static final int TAG_PADDING                           = 0xEA1C;
+
+    public static final int TAG_LENS                              = 0xFDEA;
+
+    protected static void addExifTagNames(HashMap<Integer, String> map)
+    {
+        map.put(TAG_INTEROP_INDEX, "Interoperability Index");
+        map.put(TAG_INTEROP_VERSION, "Interoperability Version");
+        map.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type");
+        map.put(TAG_SUBFILE_TYPE, "Subfile Type");
+        map.put(TAG_IMAGE_WIDTH, "Image Width");
+        map.put(TAG_IMAGE_HEIGHT, "Image Height");
+        map.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample");
+        map.put(TAG_COMPRESSION, "Compression");
+        map.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation");
+        map.put(TAG_THRESHOLDING, "Thresholding");
+        map.put(TAG_FILL_ORDER, "Fill Order");
+        map.put(TAG_DOCUMENT_NAME, "Document Name");
+        map.put(TAG_IMAGE_DESCRIPTION, "Image Description");
+        map.put(TAG_MAKE, "Make");
+        map.put(TAG_MODEL, "Model");
+        map.put(TAG_STRIP_OFFSETS, "Strip Offsets");
+        map.put(TAG_ORIENTATION, "Orientation");
+        map.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel");
+        map.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
+        map.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
+        map.put(TAG_MIN_SAMPLE_VALUE, "Minimum sample value");
+        map.put(TAG_MAX_SAMPLE_VALUE, "Maximum sample value");
+        map.put(TAG_X_RESOLUTION, "X Resolution");
+        map.put(TAG_Y_RESOLUTION, "Y Resolution");
+        map.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration");
+        map.put(TAG_PAGE_NAME, "Page Name");
+        map.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
+        map.put(TAG_TRANSFER_FUNCTION, "Transfer Function");
+        map.put(TAG_SOFTWARE, "Software");
+        map.put(TAG_DATETIME, "Date/Time");
+        map.put(TAG_ARTIST, "Artist");
+        map.put(TAG_PREDICTOR, "Predictor");
+        map.put(TAG_HOST_COMPUTER, "Host Computer");
+        map.put(TAG_WHITE_POINT, "White Point");
+        map.put(TAG_PRIMARY_CHROMATICITIES, "Primary Chromaticities");
+        map.put(TAG_TILE_WIDTH, "Tile Width");
+        map.put(TAG_TILE_LENGTH, "Tile Length");
+        map.put(TAG_TILE_OFFSETS, "Tile Offsets");
+        map.put(TAG_TILE_BYTE_COUNTS, "Tile Byte Counts");
+        map.put(TAG_SUB_IFD_OFFSET, "Sub IFD Pointer(s)");
+        map.put(TAG_TRANSFER_RANGE, "Transfer Range");
+        map.put(TAG_JPEG_TABLES, "JPEG Tables");
+        map.put(TAG_JPEG_PROC, "JPEG Proc");
+        map.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
+        map.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
+        map.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
+        map.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+        map.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format");
+        map.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width");
+        map.put(TAG_RELATED_IMAGE_HEIGHT, "Related Image Height");
+        map.put(TAG_RATING, "Rating");
+        map.put(TAG_CFA_REPEAT_PATTERN_DIM, "CFA Repeat Pattern Dim");
+        map.put(TAG_CFA_PATTERN_2, "CFA Pattern");
+        map.put(TAG_BATTERY_LEVEL, "Battery Level");
+        map.put(TAG_COPYRIGHT, "Copyright");
+        map.put(TAG_EXPOSURE_TIME, "Exposure Time");
+        map.put(TAG_FNUMBER, "F-Number");
+        map.put(TAG_IPTC_NAA, "IPTC/NAA");
+        map.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile");
+        map.put(TAG_EXPOSURE_PROGRAM, "Exposure Program");
+        map.put(TAG_SPECTRAL_SENSITIVITY, "Spectral Sensitivity");
+        map.put(TAG_ISO_EQUIVALENT, "ISO Speed Ratings");
+        map.put(TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, "Opto-electric Conversion Function (OECF)");
+        map.put(TAG_INTERLACE, "Interlace");
+        map.put(TAG_TIME_ZONE_OFFSET_TIFF_EP, "Time Zone Offset");
+        map.put(TAG_SELF_TIMER_MODE_TIFF_EP, "Self Timer Mode");
+        map.put(TAG_SENSITIVITY_TYPE, "Sensitivity Type");
+        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_EXIF_VERSION, "Exif Version");
+        map.put(TAG_DATETIME_ORIGINAL, "Date/Time Original");
+        map.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized");
+        map.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration");
+        map.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel");
+        map.put(TAG_SHUTTER_SPEED, "Shutter Speed Value");
+        map.put(TAG_APERTURE, "Aperture Value");
+        map.put(TAG_BRIGHTNESS_VALUE, "Brightness Value");
+        map.put(TAG_EXPOSURE_BIAS, "Exposure Bias Value");
+        map.put(TAG_MAX_APERTURE, "Max Aperture Value");
+        map.put(TAG_SUBJECT_DISTANCE, "Subject Distance");
+        map.put(TAG_METERING_MODE, "Metering Mode");
+        map.put(TAG_LIGHT_SOURCE, "Light Source");
+        map.put(TAG_WHITE_BALANCE, "White Balance");
+        map.put(TAG_FLASH, "Flash");
+        map.put(TAG_FOCAL_LENGTH, "Focal Length");
+        map.put(TAG_FLASH_ENERGY_TIFF_EP, "Flash Energy");
+        map.put(TAG_SPATIAL_FREQ_RESPONSE_TIFF_EP, "Spatial Frequency Response");
+        map.put(TAG_NOISE, "Noise");
+        map.put(TAG_FOCAL_PLANE_X_RESOLUTION_TIFF_EP, "Focal Plane X Resolution");
+        map.put(TAG_FOCAL_PLANE_Y_RESOLUTION_TIFF_EP, "Focal Plane Y Resolution");
+        map.put(TAG_IMAGE_NUMBER, "Image Number");
+        map.put(TAG_SECURITY_CLASSIFICATION, "Security Classification");
+        map.put(TAG_IMAGE_HISTORY, "Image History");
+        map.put(TAG_SUBJECT_LOCATION_TIFF_EP, "Subject Location");
+        map.put(TAG_EXPOSURE_INDEX_TIFF_EP, "Exposure Index");
+        map.put(TAG_STANDARD_ID_TIFF_EP, "TIFF/EP Standard ID");
+        map.put(TAG_MAKERNOTE, "Makernote");
+        map.put(TAG_USER_COMMENT, "User Comment");
+        map.put(TAG_SUBSECOND_TIME, "Sub-Sec Time");
+        map.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original");
+        map.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized");
+        map.put(TAG_WIN_TITLE, "Windows XP Title");
+        map.put(TAG_WIN_COMMENT, "Windows XP Comment");
+        map.put(TAG_WIN_AUTHOR, "Windows XP Author");
+        map.put(TAG_WIN_KEYWORDS, "Windows XP Keywords");
+        map.put(TAG_WIN_SUBJECT, "Windows XP Subject");
+        map.put(TAG_FLASHPIX_VERSION, "FlashPix Version");
+        map.put(TAG_COLOR_SPACE, "Color Space");
+        map.put(TAG_EXIF_IMAGE_WIDTH, "Exif Image Width");
+        map.put(TAG_EXIF_IMAGE_HEIGHT, "Exif Image Height");
+        map.put(TAG_RELATED_SOUND_FILE, "Related Sound File");
+        map.put(TAG_FLASH_ENERGY, "Flash Energy");
+        map.put(TAG_SPATIAL_FREQ_RESPONSE, "Spatial Frequency Response");
+        map.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution");
+        map.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution");
+        map.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit");
+        map.put(TAG_SUBJECT_LOCATION, "Subject Location");
+        map.put(TAG_EXPOSURE_INDEX, "Exposure Index");
+        map.put(TAG_SENSING_METHOD, "Sensing Method");
+        map.put(TAG_FILE_SOURCE, "File Source");
+        map.put(TAG_SCENE_TYPE, "Scene Type");
+        map.put(TAG_CFA_PATTERN, "CFA Pattern");
+        map.put(TAG_CUSTOM_RENDERED, "Custom Rendered");
+        map.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        map.put(TAG_WHITE_BALANCE_MODE, "White Balance Mode");
+        map.put(TAG_DIGITAL_ZOOM_RATIO, "Digital Zoom Ratio");
+        map.put(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, "Focal Length 35");
+        map.put(TAG_SCENE_CAPTURE_TYPE, "Scene Capture Type");
+        map.put(TAG_GAIN_CONTROL, "Gain Control");
+        map.put(TAG_CONTRAST, "Contrast");
+        map.put(TAG_SATURATION, "Saturation");
+        map.put(TAG_SHARPNESS, "Sharpness");
+        map.put(TAG_DEVICE_SETTING_DESCRIPTION, "Device Setting Description");
+        map.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range");
+        map.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID");
+        map.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name");
+        map.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number");
+        map.put(TAG_LENS_SPECIFICATION, "Lens Specification");
+        map.put(TAG_LENS_MAKE, "Lens Make");
+        map.put(TAG_LENS_MODEL, "Lens Model");
+        map.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        map.put(TAG_GAMMA, "Gamma");
+        map.put(TAG_PRINT_IM, "Print IM");
+        map.put(TAG_PANASONIC_TITLE, "Panasonic Title");
+        map.put(TAG_PANASONIC_TITLE_2, "Panasonic Title (2)");
+        map.put(TAG_PADDING, "Padding");
+        map.put(TAG_LENS, "Lens");
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifIFD0Descriptor.java b/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
new file mode 100755
index 0000000..65d121a
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ExifIFD0Directory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifIFD0Descriptor extends ExifDescriptorBase<ExifIFD0Directory>
+{
+    public ExifIFD0Descriptor(@NotNull ExifIFD0Directory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifIFD0Directory.java b/src/com/drew/metadata/exif/ExifIFD0Directory.java
new file mode 100755
index 0000000..7d2f3c7
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifIFD0Directory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.util.HashMap;
+
+/**
+ * Describes Exif tags from the IFD0 directory.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifIFD0Directory extends ExifDirectoryBase
+{
+    /** This tag is a pointer to the Exif SubIFD. */
+    public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769;
+
+    /** This tag is a pointer to the Exif GPS IFD. */
+    public static final int TAG_GPS_INFO_OFFSET = 0x8825;
+
+    public ExifIFD0Directory()
+    {
+        this.setDescriptor(new ExifIFD0Descriptor(this));
+    }
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Exif IFD0";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifInteropDescriptor.java b/src/com/drew/metadata/exif/ExifInteropDescriptor.java
new file mode 100644
index 0000000..6424db7
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifInteropDescriptor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ExifInteropDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifInteropDescriptor extends ExifDescriptorBase<ExifInteropDirectory>
+{
+    public ExifInteropDescriptor(@NotNull ExifInteropDirectory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifInteropDirectory.java b/src/com/drew/metadata/exif/ExifInteropDirectory.java
new file mode 100644
index 0000000..6d5db78
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifInteropDirectory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.util.HashMap;
+
+/**
+ * Describes Exif interoperability tags.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifInteropDirectory extends ExifDirectoryBase
+{
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+    }
+
+    public ExifInteropDirectory()
+    {
+        this.setDescriptor(new ExifInteropDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Interoperability";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifReader.java b/src/com/drew/metadata/exif/ExifReader.java
new file mode 100644
index 0000000..85f1bff
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifReader.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.imaging.tiff.TiffProcessingException;
+import com.drew.imaging.tiff.TiffReader;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory},
+ * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera
+ * makernote directories.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifReader implements JpegSegmentMetadataReader
+{
+    /** Exif data stored in JPEG files' APP1 segment are preceded by this six character preamble. */
+    public static final String JPEG_SEGMENT_PREAMBLE = "Exif\0\0";
+
+    private boolean _storeThumbnailBytes = true;
+
+    public boolean isStoreThumbnailBytes()
+    {
+        return _storeThumbnailBytes;
+    }
+
+    public void setStoreThumbnailBytes(boolean storeThumbnailBytes)
+    {
+        _storeThumbnailBytes = storeThumbnailBytes;
+    }
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Arrays.asList(JpegSegmentType.APP1);
+    }
+
+    public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
+    {
+        assert(segmentType == JpegSegmentType.APP1);
+
+        for (byte[] segmentBytes : segments) {
+            // Filter any segments containing unexpected preambles
+            if (segmentBytes.length < JPEG_SEGMENT_PREAMBLE.length() || !new String(segmentBytes, 0, JPEG_SEGMENT_PREAMBLE.length()).equals(JPEG_SEGMENT_PREAMBLE))
+                continue;
+            extract(new ByteArrayReader(segmentBytes), metadata, JPEG_SEGMENT_PREAMBLE.length());
+        }
+    }
+
+    /** Reads TIFF formatted Exif data from start of the specified {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
+    {
+        extract(reader, metadata, 0);
+    }
+
+    /** Reads TIFF formatted Exif data a specified offset within a {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset)
+    {
+        try {
+            // Read the TIFF-formatted Exif data
+            new TiffReader().processTiff(
+                reader,
+                new ExifTiffHandler(metadata, _storeThumbnailBytes),
+                readerOffset
+            );
+        } catch (TiffProcessingException e) {
+            // TODO what do to with this error state?
+            e.printStackTrace(System.err);
+        } catch (IOException e) {
+            // TODO what do to with this error state?
+            e.printStackTrace(System.err);
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java b/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
new file mode 100755
index 0000000..cd5c30e
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ExifSubIFDDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifSubIFDDescriptor extends ExifDescriptorBase<ExifSubIFDDirectory>
+{
+    public ExifSubIFDDescriptor(@NotNull ExifSubIFDDirectory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifSubIFDDirectory.java b/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
new file mode 100755
index 0000000..b05c03f
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.util.HashMap;
+
+/**
+ * Describes Exif tags from the SubIFD directory.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifSubIFDDirectory extends ExifDirectoryBase
+{
+    /** This tag is a pointer to the Exif Interop IFD. */
+    public static final int TAG_INTEROP_OFFSET = 0xA005;
+
+    public ExifSubIFDDirectory()
+    {
+        this.setDescriptor(new ExifSubIFDDescriptor(this));
+    }
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Exif SubIFD";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java b/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
new file mode 100755
index 0000000..cba06db
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ExifThumbnailDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifThumbnailDescriptor extends ExifDescriptorBase<ExifThumbnailDirectory>
+{
+    public ExifThumbnailDescriptor(@NotNull ExifThumbnailDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_THUMBNAIL_OFFSET:
+                return getThumbnailOffsetDescription();
+            case TAG_THUMBNAIL_LENGTH:
+                return getThumbnailLengthDescription();
+            case TAG_THUMBNAIL_COMPRESSION:
+                return getCompressionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getCompressionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_THUMBNAIL_COMPRESSION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Uncompressed";
+            case 2: return "CCITT 1D";
+            case 3: return "T4/Group 3 Fax";
+            case 4: return "T6/Group 4 Fax";
+            case 5: return "LZW";
+            case 6: return "JPEG (old-style)";
+            case 7: return "JPEG";
+            case 8: return "Adobe Deflate";
+            case 9: return "JBIG B&W";
+            case 10: return "JBIG Color";
+            case 32766: return "Next";
+            case 32771: return "CCIRLEW";
+            case 32773: return "PackBits";
+            case 32809: return "Thunderscan";
+            case 32895: return "IT8CTPAD";
+            case 32896: return "IT8LW";
+            case 32897: return "IT8MP";
+            case 32898: return "IT8BL";
+            case 32908: return "PixarFilm";
+            case 32909: return "PixarLog";
+            case 32946: return "Deflate";
+            case 32947: return "DCS";
+            case 32661: return "JBIG";
+            case 32676: return "SGILog";
+            case 32677: return "SGILog24";
+            case 32712: return "JPEG 2000";
+            case 32713: return "Nikon NEF Compressed";
+            default:
+                return "Unknown compression";
+        }
+    }
+
+    @Nullable
+    public String getThumbnailLengthDescription()
+    {
+        String value = _directory.getString(TAG_THUMBNAIL_LENGTH);
+        return value == null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getThumbnailOffsetDescription()
+    {
+        String value = _directory.getString(TAG_THUMBNAIL_OFFSET);
+        return value == null ? null : value + " bytes";
+    }
+}
diff --git a/src/com/drew/metadata/exif/ExifThumbnailDirectory.java b/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
new file mode 100755
index 0000000..a73f005
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.MetadataException;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * One of several Exif directories.  Otherwise known as IFD1, this directory holds information about an embedded thumbnail image.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifThumbnailDirectory extends ExifDirectoryBase
+{
+    /**
+     * The offset to thumbnail image bytes.
+     */
+    public static final int TAG_THUMBNAIL_OFFSET = 0x0201;
+    /**
+     * The size of the thumbnail image data in bytes.
+     */
+    public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
+
+    /**
+     * Shows compression method for Thumbnail.
+     * 1 = Uncompressed
+     * 2 = CCITT 1D
+     * 3 = T4/Group 3 Fax
+     * 4 = T6/Group 4 Fax
+     * 5 = LZW
+     * 6 = JPEG (old-style)
+     * 7 = JPEG
+     * 8 = Adobe Deflate
+     * 9 = JBIG B&W
+     * 10 = JBIG Color
+     * 32766 = Next
+     * 32771 = CCIRLEW
+     * 32773 = PackBits
+     * 32809 = Thunderscan
+     * 32895 = IT8CTPAD
+     * 32896 = IT8LW
+     * 32897 = IT8MP
+     * 32898 = IT8BL
+     * 32908 = PixarFilm
+     * 32909 = PixarLog
+     * 32946 = Deflate
+     * 32947 = DCS
+     * 34661 = JBIG
+     * 34676 = SGILog
+     * 34677 = SGILog24
+     * 34712 = JPEG 2000
+     * 34713 = Nikon NEF Compressed
+     */
+    public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+
+        _tagNameMap.put(TAG_THUMBNAIL_COMPRESSION, "Thumbnail Compression");
+        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
+    }
+
+    @Nullable
+    private byte[] _thumbnailData;
+
+    public ExifThumbnailDirectory()
+    {
+        this.setDescriptor(new ExifThumbnailDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Exif Thumbnail";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    public boolean hasThumbnailData()
+    {
+        return _thumbnailData != null;
+    }
+
+    @Nullable
+    public byte[] getThumbnailData()
+    {
+        return _thumbnailData;
+    }
+
+    public void setThumbnailData(@Nullable byte[] data)
+    {
+        _thumbnailData = data;
+    }
+
+    public void writeThumbnail(@NotNull String filename) throws MetadataException, IOException
+    {
+        byte[] data = _thumbnailData;
+
+        if (data == null)
+            throw new MetadataException("No thumbnail data exists.");
+
+        FileOutputStream stream = null;
+        try {
+            stream = new FileOutputStream(filename);
+            stream.write(data);
+        } finally {
+            if (stream != null)
+                stream.close();
+        }
+    }
+
+/*
+    // This thumbnail extraction code is not complete, and is included to assist anyone who feels like looking into
+    // it.  Please share any progress with the original author, and hence the community.  Thanks.
+
+    public Image getThumbnailImage() throws MetadataException
+    {
+        if (!hasThumbnailData())
+            return null;
+
+        int compression = 0;
+        try {
+            compression = this.getInt(ExifSubIFDDirectory.TAG_COMPRESSION);
+        } catch (Throwable e) {
+            this.addError("Unable to determine thumbnail type " + e.getMessage());
+        }
+
+        final byte[] thumbnailBytes = getThumbnailData();
+
+        if (compression == ExifSubIFDDirectory.COMPRESSION_JPEG)
+        {
+            // JPEG Thumbnail
+            // operate directly on thumbnailBytes
+            return decodeBytesAsImage(thumbnailBytes);
+        }
+        else if (compression == ExifSubIFDDirectory.COMPRESSION_NONE)
+        {
+            // uncompressed thumbnail (raw RGB data)
+            if (!this.containsTag(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION))
+                return null;
+
+            try
+            {
+                // If the image is RGB format, then convert it to a bitmap
+                final int photometricInterpretation = this.getInt(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
+                if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_RGB)
+                {
+                    // RGB
+                    Image image = createImageFromRawRgb(thumbnailBytes);
+                    return image;
+                }
+                else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_YCBCR)
+                {
+                    // YCbCr
+                    Image image = createImageFromRawYCbCr(thumbnailBytes);
+                    return image;
+                }
+                else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_MONOCHROME)
+                {
+                    // Monochrome
+                    return null;
+                }
+            } catch (Throwable e) {
+                this.addError("Unable to extract thumbnail: " + e.getMessage());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Handle the YCbCr thumbnail encoding used by Ricoh RDC4200/4300, Fuji DS-7/300 and DX-5/7/9 cameras.
+     *
+     * At DX-5/7/9, YCbCrSubsampling(0x0212) has values of '2,1', PlanarConfiguration(0x011c) has a value '1'. So the
+     * data align of this image is below.
+     *
+     * Y(0,0),Y(1,0),Cb(0,0),Cr(0,0), Y(2,0),Y(3,0),Cb(2,0),Cr(3.0), Y(4,0),Y(5,0),Cb(4,0),Cr(4,0). . . .
+     *
+     * The numbers in parenthesis are pixel coordinates. DX series' YCbCrCoefficients(0x0211) has values '0.299/0.587/0.114',
+     * ReferenceBlackWhite(0x0214) has values '0,255,128,255,128,255'. Therefore to convert from Y/Cb/Cr to RGB is;
+     *
+     * B(0,0)=(Cb-128)*(2-0.114*2)+Y(0,0)
+     * R(0,0)=(Cr-128)*(2-0.299*2)+Y(0,0)
+     * G(0,0)=(Y(0,0)-0.114*B(0,0)-0.299*R(0,0))/0.587
+     *
+     * Horizontal subsampling is a value '2', so you can calculate B(1,0)/R(1,0)/G(1,0) by using the Y(1,0) and Cr(0,0)/Cb(0,0).
+     * Repeat this conversion by value of ImageWidth(0x0100) and ImageLength(0x0101).
+     *
+     * @param thumbnailBytes
+     * @return
+     * @throws com.drew.metadata.MetadataException
+     * /
+    private Image createImageFromRawYCbCr(byte[] thumbnailBytes) throws MetadataException
+    {
+        /*
+            Y  =  0.257R + 0.504G + 0.098B + 16
+            Cb = -0.148R - 0.291G + 0.439B + 128
+            Cr =  0.439R - 0.368G - 0.071B + 128
+
+            G = 1.164(Y-16) - 0.391(Cb-128) - 0.813(Cr-128)
+            R = 1.164(Y-16) + 1.596(Cr-128)
+            B = 1.164(Y-16) + 2.018(Cb-128)
+
+            R, G and B range from 0 to 255.
+            Y ranges from 16 to 235.
+            Cb and Cr range from 16 to 240.
+
+            http://www.faqs.org/faqs/graphics/colorspace-faq/
+        * /
+
+        int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
+        final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
+        final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
+//        final int headerLength = 54;
+//        byte[] result = new byte[length + headerLength];
+//        // Add a windows BMP header described:
+//        // http://www.onicos.com/staff/iz/formats/bmp.html
+//        result[0] = 'B';
+//        result[1] = 'M'; // File Type identifier
+//        result[3] = (byte)(result.length / 256);
+//        result[2] = (byte)result.length;
+//        result[10] = (byte)headerLength;
+//        result[14] = 40; // MS Windows BMP header
+//        result[18] = (byte)imageWidth;
+//        result[22] = (byte)imageHeight;
+//        result[26] = 1;  // 1 Plane
+//        result[28] = 24; // Colour depth
+//        result[34] = (byte)length;
+//        result[35] = (byte)(length / 256);
+
+        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
+
+        // order is YCbCr and image is upside down, bitmaps are BGR
+////        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
+//        {
+//            final int y =  thumbnailBytes[dataOffset - 2] & 0xFF;
+//            final int cb = thumbnailBytes[dataOffset - 1] & 0xFF;
+//            final int cr = thumbnailBytes[dataOffset] & 0xFF;
+//            if (y<16 || y>235 || cb<16 || cb>240 || cr<16 || cr>240)
+//                "".toString();
+//
+//            int g = (int)(1.164*(y-16) - 0.391*(cb-128) - 0.813*(cr-128));
+//            int r = (int)(1.164*(y-16) + 1.596*(cr-128));
+//            int b = (int)(1.164*(y-16) + 2.018*(cb-128));
+//
+////            result[i] = (byte)b;
+////            result[i + 1] = (byte)g;
+////            result[i + 2] = (byte)r;
+//
+//            // TODO compose the image here
+//            image.setRGB(1, 2, 3);
+//        }
+
+        return image;
+    }
+
+    /**
+     * Creates a thumbnail image in (Windows) BMP format from raw RGB data.
+     * @param thumbnailBytes
+     * @return
+     * @throws com.drew.metadata.MetadataException
+     * /
+    private Image createImageFromRawRgb(byte[] thumbnailBytes) throws MetadataException
+    {
+        final int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
+        final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
+        final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
+//        final int headerLength = 54;
+//        final byte[] result = new byte[length + headerLength];
+//        // Add a windows BMP header described:
+//        // http://www.onicos.com/staff/iz/formats/bmp.html
+//        result[0] = 'B';
+//        result[1] = 'M'; // File Type identifier
+//        result[3] = (byte)(result.length / 256);
+//        result[2] = (byte)result.length;
+//        result[10] = (byte)headerLength;
+//        result[14] = 40; // MS Windows BMP header
+//        result[18] = (byte)imageWidth;
+//        result[22] = (byte)imageHeight;
+//        result[26] = 1;  // 1 Plane
+//        result[28] = 24; // Colour depth
+//        result[34] = (byte)length;
+//        result[35] = (byte)(length / 256);
+
+        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
+
+        // order is RGB and image is upside down, bitmaps are BGR
+//        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
+//        {
+//            byte b = thumbnailBytes[dataOffset - 2];
+//            byte g = thumbnailBytes[dataOffset - 1];
+//            byte r = thumbnailBytes[dataOffset];
+//
+//            // TODO compose the image here
+//            image.setRGB(1, 2, 3);
+//        }
+
+        return image;
+    }
+*/
+}
diff --git a/src/com/drew/metadata/exif/ExifTiffHandler.java b/src/com/drew/metadata/exif/ExifTiffHandler.java
new file mode 100644
index 0000000..eab5feb
--- /dev/null
+++ b/src/com/drew/metadata/exif/ExifTiffHandler.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.imaging.tiff.TiffProcessingException;
+import com.drew.imaging.tiff.TiffReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.exif.makernotes.*;
+import com.drew.metadata.iptc.IptcReader;
+import com.drew.metadata.tiff.DirectoryTiffHandler;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Implementation of {@link com.drew.imaging.tiff.TiffHandler} used for handling TIFF tags according to the Exif
+ * standard.
+ * <p>
+ * Includes support for camera manufacturer makernotes.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class ExifTiffHandler extends DirectoryTiffHandler
+{
+    private final boolean _storeThumbnailBytes;
+
+    public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes)
+    {
+        super(metadata, ExifIFD0Directory.class);
+        _storeThumbnailBytes = storeThumbnailBytes;
+    }
+
+    public void setTiffMarker(int marker) throws TiffProcessingException
+    {
+        final int standardTiffMarker = 0x002A;
+        final int olympusRawTiffMarker = 0x4F52; // for ORF files
+        final int olympusRawTiffMarker2 = 0x5352; // for ORF files
+        final int panasonicRawTiffMarker = 0x0055; // for RW2 files
+
+        if (marker != standardTiffMarker && marker != olympusRawTiffMarker && marker != olympusRawTiffMarker2 && marker != panasonicRawTiffMarker) {
+            throw new TiffProcessingException("Unexpected TIFF marker: 0x" + Integer.toHexString(marker));
+        }
+    }
+
+    public boolean isTagIfdPointer(int tagType)
+    {
+        if (tagType == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
+            pushDirectory(ExifSubIFDDirectory.class);
+            return true;
+        } else if (tagType == ExifIFD0Directory.TAG_GPS_INFO_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
+            pushDirectory(GpsDirectory.class);
+            return true;
+        } else if (tagType == ExifSubIFDDirectory.TAG_INTEROP_OFFSET && _currentDirectory instanceof ExifSubIFDDirectory) {
+            pushDirectory(ExifInteropDirectory.class);
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean hasFollowerIfd()
+    {
+        // In Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case.
+        if (_currentDirectory instanceof ExifIFD0Directory) {
+            pushDirectory(ExifThumbnailDirectory.class);
+            return true;
+        }
+
+        // The Canon EOS 7D (CR2) has three chained/following thumbnail IFDs
+        if (_currentDirectory instanceof ExifThumbnailDirectory)
+            return true;
+
+        // This should not happen, as Exif doesn't use follower IFDs apart from that above.
+        // NOTE have seen the CanonMakernoteDirectory IFD have a follower pointer, but it points to invalid data.
+        return false;
+    }
+
+    public boolean customProcessTag(final int tagOffset,
+                                    final @NotNull Set<Integer> processedIfdOffsets,
+                                    final int tiffHeaderOffset,
+                                    final @NotNull RandomAccessReader reader,
+                                    final int tagId,
+                                    final int byteCount) throws IOException
+    {
+        // Custom processing for the Makernote tag
+        if (tagId == ExifSubIFDDirectory.TAG_MAKERNOTE && _currentDirectory instanceof ExifSubIFDDirectory) {
+            return processMakernote(tagOffset, processedIfdOffsets, tiffHeaderOffset, reader);
+        }
+
+        // Custom processing for embedded IPTC data
+        if (tagId == ExifSubIFDDirectory.TAG_IPTC_NAA && _currentDirectory instanceof ExifIFD0Directory) {
+            // NOTE Adobe sets type 4 for IPTC instead of 7
+            if (reader.getInt8(tagOffset) == 0x1c) {
+                final byte[] iptcBytes = reader.getBytes(tagOffset, byteCount);
+                new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length);
+                return true;
+            }
+            return false;
+        }
+
+        return false;
+    }
+
+    public void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset)
+    {
+        if (_storeThumbnailBytes) {
+            // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
+            ExifThumbnailDirectory thumbnailDirectory = _metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
+            if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
+                Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
+                Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
+                if (offset != null && length != null) {
+                    try {
+                        byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);
+                        thumbnailDirectory.setThumbnailData(thumbnailData);
+                    } catch (IOException ex) {
+                        thumbnailDirectory.addError("Invalid thumbnail data specification: " + ex.getMessage());
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean processMakernote(final int makernoteOffset,
+                                     final @NotNull Set<Integer> processedIfdOffsets,
+                                     final int tiffHeaderOffset,
+                                     final @NotNull RandomAccessReader reader) throws IOException
+    {
+        // Determine the camera model and makernote format.
+        Directory ifd0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+
+        if (ifd0Directory == null)
+            return false;
+
+        String cameraMake = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
+
+        final String firstTwoChars = reader.getString(makernoteOffset, 2);
+        final String firstThreeChars = reader.getString(makernoteOffset, 3);
+        final String firstFourChars = reader.getString(makernoteOffset, 4);
+        final String firstFiveChars = reader.getString(makernoteOffset, 5);
+        final String firstSixChars = reader.getString(makernoteOffset, 6);
+        final String firstSevenChars = reader.getString(makernoteOffset, 7);
+        final String firstEightChars = reader.getString(makernoteOffset, 8);
+        final String firstTwelveChars = reader.getString(makernoteOffset, 12);
+
+        boolean byteOrderBefore = reader.isMotorolaByteOrder();
+
+        if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
+            // Olympus Makernote
+            // Epson and Agfa use Olympus makernote standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/
+            pushDirectory(OlympusMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+        } else if (cameraMake != null && cameraMake.toUpperCase().startsWith("MINOLTA")) {
+            // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote
+            // area that commences immediately.
+            pushDirectory(OlympusMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+        } else if (cameraMake != null && cameraMake.trim().toUpperCase().startsWith("NIKON")) {
+            if ("Nikon".equals(firstFiveChars)) {
+                /* There are two scenarios here:
+                 * Type 1:                  **
+                 * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
+                 * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
+                 * Type 3:                  **
+                 * :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*...
+                 * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200
+                 */
+                switch (reader.getUInt8(makernoteOffset + 6)) {
+                    case 1:
+                        pushDirectory(NikonType1MakernoteDirectory.class);
+                        TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+                        break;
+                    case 2:
+                        pushDirectory(NikonType2MakernoteDirectory.class);
+                        TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 18, makernoteOffset + 10);
+                        break;
+                    default:
+                        ifd0Directory.addError("Unsupported Nikon makernote data ignored.");
+                        break;
+                }
+            } else {
+                // The IFD begins with the first Makernote byte (no ASCII name).  This occurs with CoolPix 775, E990 and D1 models.
+                pushDirectory(NikonType2MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+            }
+        } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {
+            pushDirectory(SonyType1MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset);
+        } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
+            // force MM for this directory
+            reader.setMotorolaByteOrder(true);
+            // skip 12 byte header + 2 for "MM" + 6
+            pushDirectory(SonyType6MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 20, tiffHeaderOffset);
+        } else if ("SIGMA\u0000\u0000\u0000".equals(firstEightChars) || "FOVEON\u0000\u0000".equals(firstEightChars)) {
+            pushDirectory(SigmaMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 10, tiffHeaderOffset);
+        } else if ("KDK".equals(firstThreeChars)) {
+            reader.setMotorolaByteOrder(firstSevenChars.equals("KDK INFO"));
+            KodakMakernoteDirectory directory = new KodakMakernoteDirectory();
+            _metadata.addDirectory(directory);
+            processKodakMakernote(directory, makernoteOffset, reader);
+        } else if ("Canon".equalsIgnoreCase(cameraMake)) {
+            pushDirectory(CanonMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+        } else if (cameraMake != null && cameraMake.toUpperCase().startsWith("CASIO")) {
+            if ("QVC\u0000\u0000\u0000".equals(firstSixChars)) {
+                pushDirectory(CasioType2MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 6, tiffHeaderOffset);
+            } else {
+                pushDirectory(CasioType1MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
+            }
+        } else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraMake)) {
+            // Note that this also applies to certain Leica cameras, such as the Digilux-4.3
+            reader.setMotorolaByteOrder(false);
+            // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote
+            // IFD, though the offset is relative to the start of the makernote, not the TIFF
+            // header (like everywhere else)
+            int ifdStart = makernoteOffset + reader.getInt32(makernoteOffset + 8);
+            pushDirectory(FujifilmMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, ifdStart, makernoteOffset);
+        } else if ("KYOCERA".equals(firstSevenChars)) {
+            // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
+            pushDirectory(KyoceraMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 22, tiffHeaderOffset);
+        } else if ("LEICA".equals(firstFiveChars)) {
+            reader.setMotorolaByteOrder(false);
+            if ("Leica Camera AG".equals(cameraMake)) {
+                pushDirectory(LeicaMakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+            } else if ("LEICA".equals(cameraMake)) {
+                // Some Leica cameras use Panasonic makernote tags
+                pushDirectory(PanasonicMakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+            } else {
+                return false;
+            }
+        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12))) {
+            // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD
+            // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment
+            // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html
+            pushDirectory(PanasonicMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset);
+        } else if ("AOC\u0000".equals(firstFourChars)) {
+            // NON-Standard TIFF IFD Data using Casio Type 2 Tags
+            // IFD has no Next-IFD pointer at end of IFD, and
+            // Offsets are relative to the start of the current IFD tag, not the TIFF header
+            // Observed for:
+            // - Pentax ist D
+            pushDirectory(CasioType2MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 6, makernoteOffset);
+        } else if (cameraMake != null && (cameraMake.toUpperCase().startsWith("PENTAX") || cameraMake.toUpperCase().startsWith("ASAHI"))) {
+            // NON-Standard TIFF IFD Data using Pentax Tags
+            // IFD has no Next-IFD pointer at end of IFD, and
+            // Offsets are relative to the start of the current IFD tag, not the TIFF header
+            // Observed for:
+            // - PENTAX Optio 330
+            // - PENTAX Optio 430
+            pushDirectory(PentaxMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, makernoteOffset);
+//        } else if ("KC".equals(firstTwoChars) || "MINOL".equals(firstFiveChars) || "MLY".equals(firstThreeChars) || "+M+M+M+M".equals(firstEightChars)) {
+//            // This Konica data is not understood.  Header identified in accordance with information at this site:
+//            // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html
+//            // TODO add support for minolta/konica cameras
+//            exifDirectory.addError("Unsupported Konica/Minolta data ignored.");
+        } else if ("SANYO\0\1\0".equals(firstEightChars)) {
+            pushDirectory(SanyoMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
+        } else if (cameraMake != null && cameraMake.toLowerCase().startsWith("ricoh")) {
+            if (firstTwoChars.equals("Rv") || firstThreeChars.equals("Rev")) {
+                // This is a textual format, where the makernote bytes look like:
+                //   Rv0103;Rg1C;Bg18;Ll0;Ld0;Aj0000;Bn0473800;Fp2E00:������������������������������
+                //   Rv0103;Rg1C;Bg18;Ll0;Ld0;Aj0000;Bn0473800;Fp2D05:������������������������������
+                //   Rv0207;Sf6C84;Rg76;Bg60;Gg42;Ll0;Ld0;Aj0004;Bn0B02900;Fp10B8;Md6700;Ln116900086D27;Sv263:0000000000000000000000��
+                // This format is currently unsupported
+                return false;
+            } else if (firstFiveChars.equalsIgnoreCase("Ricoh")) {
+                // Always in Motorola byte order
+                reader.setMotorolaByteOrder(true);
+                pushDirectory(RicohMakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
+            }
+        } else {
+            // The makernote is not comprehended by this library.
+            // If you are reading this and believe a particular camera's image should be processed, get in touch.
+            return false;
+        }
+
+        reader.setMotorolaByteOrder(byteOrderBefore);
+        return true;
+    }
+
+    private static void processKodakMakernote(@NotNull final KodakMakernoteDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader)
+    {
+        // Kodak's makernote is not in IFD format. It has values at fixed offsets.
+        int dataOffset = tagValueOffset + 8;
+        try {
+            directory.setString(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getString(dataOffset, 8));
+            directory.setInt(KodakMakernoteDirectory.TAG_QUALITY, reader.getUInt8(dataOffset + 9));
+            directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE, reader.getUInt8(dataOffset + 10));
+            directory.setInt(KodakMakernoteDirectory.TAG_IMAGE_WIDTH, reader.getUInt16(dataOffset + 12));
+            directory.setInt(KodakMakernoteDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16(dataOffset + 14));
+            directory.setInt(KodakMakernoteDirectory.TAG_YEAR_CREATED, reader.getUInt16(dataOffset + 16));
+            directory.setByteArray(KodakMakernoteDirectory.TAG_MONTH_DAY_CREATED, reader.getBytes(dataOffset + 18, 2));
+            directory.setByteArray(KodakMakernoteDirectory.TAG_TIME_CREATED, reader.getBytes(dataOffset + 20, 4));
+            directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE_2, reader.getUInt16(dataOffset + 24));
+            directory.setInt(KodakMakernoteDirectory.TAG_SHUTTER_MODE, reader.getUInt8(dataOffset + 27));
+            directory.setInt(KodakMakernoteDirectory.TAG_METERING_MODE, reader.getUInt8(dataOffset + 28));
+            directory.setInt(KodakMakernoteDirectory.TAG_SEQUENCE_NUMBER, reader.getUInt8(dataOffset + 29));
+            directory.setInt(KodakMakernoteDirectory.TAG_F_NUMBER, reader.getUInt16(dataOffset + 30));
+            directory.setLong(KodakMakernoteDirectory.TAG_EXPOSURE_TIME, reader.getUInt32(dataOffset + 32));
+            directory.setInt(KodakMakernoteDirectory.TAG_EXPOSURE_COMPENSATION, reader.getInt16(dataOffset + 36));
+            directory.setInt(KodakMakernoteDirectory.TAG_FOCUS_MODE, reader.getUInt8(dataOffset + 56));
+            directory.setInt(KodakMakernoteDirectory.TAG_WHITE_BALANCE, reader.getUInt8(dataOffset + 64));
+            directory.setInt(KodakMakernoteDirectory.TAG_FLASH_MODE, reader.getUInt8(dataOffset + 92));
+            directory.setInt(KodakMakernoteDirectory.TAG_FLASH_FIRED, reader.getUInt8(dataOffset + 93));
+            directory.setInt(KodakMakernoteDirectory.TAG_ISO_SETTING, reader.getUInt16(dataOffset + 94));
+            directory.setInt(KodakMakernoteDirectory.TAG_ISO, reader.getUInt16(dataOffset + 96));
+            directory.setInt(KodakMakernoteDirectory.TAG_TOTAL_ZOOM, reader.getUInt16(dataOffset + 98));
+            directory.setInt(KodakMakernoteDirectory.TAG_DATE_TIME_STAMP, reader.getUInt16(dataOffset + 100));
+            directory.setInt(KodakMakernoteDirectory.TAG_COLOR_MODE, reader.getUInt16(dataOffset + 102));
+            directory.setInt(KodakMakernoteDirectory.TAG_DIGITAL_ZOOM, reader.getUInt16(dataOffset + 104));
+            directory.setInt(KodakMakernoteDirectory.TAG_SHARPNESS, reader.getInt8(dataOffset + 107));
+        } catch (IOException ex) {
+            directory.addError("Error processing Kodak makernote data: " + ex.getMessage());
+        }
+    }
+}
+
diff --git a/src/com/drew/metadata/exif/GpsDescriptor.java b/src/com/drew/metadata/exif/GpsDescriptor.java
new file mode 100644
index 0000000..a3173e2
--- /dev/null
+++ b/src/com/drew/metadata/exif/GpsDescriptor.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.GeoLocation;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DecimalFormat;
+
+import static com.drew.metadata.exif.GpsDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link GpsDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class GpsDescriptor extends TagDescriptor<GpsDirectory>
+{
+    public GpsDescriptor(@NotNull GpsDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_VERSION_ID:
+                return getGpsVersionIdDescription();
+            case TAG_ALTITUDE:
+                return getGpsAltitudeDescription();
+            case TAG_ALTITUDE_REF:
+                return getGpsAltitudeRefDescription();
+            case TAG_STATUS:
+                return getGpsStatusDescription();
+            case TAG_MEASURE_MODE:
+                return getGpsMeasureModeDescription();
+            case TAG_SPEED_REF:
+                return getGpsSpeedRefDescription();
+            case TAG_TRACK_REF:
+            case TAG_IMG_DIRECTION_REF:
+            case TAG_DEST_BEARING_REF:
+                return getGpsDirectionReferenceDescription(tagType);
+            case TAG_TRACK:
+            case TAG_IMG_DIRECTION:
+            case TAG_DEST_BEARING:
+                return getGpsDirectionDescription(tagType);
+            case TAG_DEST_DISTANCE_REF:
+                return getGpsDestinationReferenceDescription();
+            case TAG_TIME_STAMP:
+                return getGpsTimeStampDescription();
+            case TAG_LONGITUDE:
+                // three rational numbers -- displayed in HH"MM"SS.ss
+                return getGpsLongitudeDescription();
+            case TAG_LATITUDE:
+                // three rational numbers -- displayed in HH"MM"SS.ss
+                return getGpsLatitudeDescription();
+            case TAG_DIFFERENTIAL:
+                return getGpsDifferentialDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getGpsVersionIdDescription()
+    {
+        return getVersionBytesDescription(TAG_VERSION_ID, 1);
+    }
+
+    @Nullable
+    public String getGpsLatitudeDescription()
+    {
+        GeoLocation location = _directory.getGeoLocation();
+        return location == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(location.getLatitude());
+    }
+
+    @Nullable
+    public String getGpsLongitudeDescription()
+    {
+        GeoLocation location = _directory.getGeoLocation();
+        return location == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(location.getLongitude());
+    }
+
+    @Nullable
+    public String getGpsTimeStampDescription()
+    {
+        // time in hour, min, sec
+        Rational[] timeComponents = _directory.getRationalArray(TAG_TIME_STAMP);
+        DecimalFormat df = new DecimalFormat("00.00");
+        return timeComponents == null
+            ? null
+            : String.format("%02d:%02d:%s UTC",
+                timeComponents[0].intValue(),
+                timeComponents[1].intValue(),
+                df.format(timeComponents[2].doubleValue()));
+    }
+
+    @Nullable
+    public String getGpsDestinationReferenceDescription()
+    {
+        final String value = _directory.getString(TAG_DEST_DISTANCE_REF);
+        if (value == null)
+            return null;
+        String distanceRef = value.trim();
+        if ("K".equalsIgnoreCase(distanceRef)) {
+            return "kilometers";
+        } else if ("M".equalsIgnoreCase(distanceRef)) {
+            return "miles";
+        } else if ("N".equalsIgnoreCase(distanceRef)) {
+            return "knots";
+        } else {
+            return "Unknown (" + distanceRef + ")";
+        }
+    }
+
+    @Nullable
+    public String getGpsDirectionDescription(int tagType)
+    {
+        Rational angle = _directory.getRational(tagType);
+        // provide a decimal version of rational numbers in the description, to avoid strings like "35334/199 degrees"
+        String value = angle != null
+            ? new DecimalFormat("0.##").format(angle.doubleValue())
+            : _directory.getString(tagType);
+        return value == null || value.trim().length() == 0 ? null : value.trim() + " degrees";
+    }
+
+    @Nullable
+    public String getGpsDirectionReferenceDescription(int tagType)
+    {
+        final String value = _directory.getString(tagType);
+        if (value == null)
+            return null;
+        String gpsDistRef = value.trim();
+        if ("T".equalsIgnoreCase(gpsDistRef)) {
+            return "True direction";
+        } else if ("M".equalsIgnoreCase(gpsDistRef)) {
+            return "Magnetic direction";
+        } else {
+            return "Unknown (" + gpsDistRef + ")";
+        }
+    }
+
+    @Nullable
+    public String getGpsSpeedRefDescription()
+    {
+        final String value = _directory.getString(TAG_SPEED_REF);
+        if (value == null)
+            return null;
+        String gpsSpeedRef = value.trim();
+        if ("K".equalsIgnoreCase(gpsSpeedRef)) {
+            return "kph";
+        } else if ("M".equalsIgnoreCase(gpsSpeedRef)) {
+            return "mph";
+        } else if ("N".equalsIgnoreCase(gpsSpeedRef)) {
+            return "knots";
+        } else {
+            return "Unknown (" + gpsSpeedRef + ")";
+        }
+    }
+
+    @Nullable
+    public String getGpsMeasureModeDescription()
+    {
+        final String value = _directory.getString(TAG_MEASURE_MODE);
+        if (value == null)
+            return null;
+        String gpsSpeedMeasureMode = value.trim();
+        if ("2".equalsIgnoreCase(gpsSpeedMeasureMode)) {
+            return "2-dimensional measurement";
+        } else if ("3".equalsIgnoreCase(gpsSpeedMeasureMode)) {
+            return "3-dimensional measurement";
+        } else {
+            return "Unknown (" + gpsSpeedMeasureMode + ")";
+        }
+    }
+
+    @Nullable
+    public String getGpsStatusDescription()
+    {
+        final String value = _directory.getString(TAG_STATUS);
+        if (value == null)
+            return null;
+        String gpsStatus = value.trim();
+        if ("A".equalsIgnoreCase(gpsStatus)) {
+            return "Active (Measurement in progress)";
+        } else if ("V".equalsIgnoreCase(gpsStatus)) {
+            return "Void (Measurement Interoperability)";
+        } else {
+            return "Unknown (" + gpsStatus + ")";
+        }
+    }
+
+    @Nullable
+    public String getGpsAltitudeRefDescription()
+    {
+        return getIndexedDescription(TAG_ALTITUDE_REF, "Sea level", "Below sea level");
+    }
+
+    @Nullable
+    public String getGpsAltitudeDescription()
+    {
+        final Rational value = _directory.getRational(TAG_ALTITUDE);
+        return value == null ? null : value.intValue() + " metres";
+    }
+
+    @Nullable
+    public String getGpsDifferentialDescription()
+    {
+        return getIndexedDescription(TAG_DIFFERENTIAL, "No Correction", "Differential Corrected");
+    }
+
+    @Nullable
+    public String getDegreesMinutesSecondsDescription()
+    {
+        GeoLocation location = _directory.getGeoLocation();
+        return location == null ? null : location.toDMSString();
+    }
+}
diff --git a/src/com/drew/metadata/exif/GpsDirectory.java b/src/com/drew/metadata/exif/GpsDirectory.java
new file mode 100644
index 0000000..755a171
--- /dev/null
+++ b/src/com/drew/metadata/exif/GpsDirectory.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.GeoLocation;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.util.HashMap;
+
+/**
+ * Describes Exif tags that contain Global Positioning System (GPS) data.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class GpsDirectory extends ExifDirectoryBase
+{
+    /** GPS tag version GPSVersionID 0 0 BYTE 4 */
+    public static final int TAG_VERSION_ID = 0x0000;
+    /** North or South Latitude GPSLatitudeRef 1 1 ASCII 2 */
+    public static final int TAG_LATITUDE_REF = 0x0001;
+    /** Latitude GPSLatitude 2 2 RATIONAL 3 */
+    public static final int TAG_LATITUDE = 0x0002;
+    /** East or West Longitude GPSLongitudeRef 3 3 ASCII 2 */
+    public static final int TAG_LONGITUDE_REF = 0x0003;
+    /** Longitude GPSLongitude 4 4 RATIONAL 3 */
+    public static final int TAG_LONGITUDE = 0x0004;
+    /** Altitude reference GPSAltitudeRef 5 5 BYTE 1 */
+    public static final int TAG_ALTITUDE_REF = 0x0005;
+    /** Altitude GPSAltitude 6 6 RATIONAL 1 */
+    public static final int TAG_ALTITUDE = 0x0006;
+    /** GPS time (atomic clock) GPSTimeStamp 7 7 RATIONAL 3 */
+    public static final int TAG_TIME_STAMP = 0x0007;
+    /** GPS satellites used for measurement GPSSatellites 8 8 ASCII Any */
+    public static final int TAG_SATELLITES = 0x0008;
+    /** GPS receiver status GPSStatus 9 9 ASCII 2 */
+    public static final int TAG_STATUS = 0x0009;
+    /** GPS measurement mode GPSMeasureMode 10 A ASCII 2 */
+    public static final int TAG_MEASURE_MODE = 0x000A;
+    /** Measurement precision GPSDOP 11 B RATIONAL 1 */
+    public static final int TAG_DOP = 0x000B;
+    /** Speed unit GPSSpeedRef 12 C ASCII 2 */
+    public static final int TAG_SPEED_REF = 0x000C;
+    /** Speed of GPS receiver GPSSpeed 13 D RATIONAL 1 */
+    public static final int TAG_SPEED = 0x000D;
+    /** Reference for direction of movement GPSTrackRef 14 E ASCII 2 */
+    public static final int TAG_TRACK_REF = 0x000E;
+    /** Direction of movement GPSTrack 15 F RATIONAL 1 */
+    public static final int TAG_TRACK = 0x000F;
+    /** Reference for direction of image GPSImgDirectionRef 16 10 ASCII 2 */
+    public static final int TAG_IMG_DIRECTION_REF = 0x0010;
+    /** Direction of image GPSImgDirection 17 11 RATIONAL 1 */
+    public static final int TAG_IMG_DIRECTION = 0x0011;
+    /** Geodetic survey data used GPSMapDatum 18 12 ASCII Any */
+    public static final int TAG_MAP_DATUM = 0x0012;
+    /** Reference for latitude of destination GPSDestLatitudeRef 19 13 ASCII 2 */
+    public static final int TAG_DEST_LATITUDE_REF = 0x0013;
+    /** Latitude of destination GPSDestLatitude 20 14 RATIONAL 3 */
+    public static final int TAG_DEST_LATITUDE = 0x0014;
+    /** Reference for longitude of destination GPSDestLongitudeRef 21 15 ASCII 2 */
+    public static final int TAG_DEST_LONGITUDE_REF = 0x0015;
+    /** Longitude of destination GPSDestLongitude 22 16 RATIONAL 3 */
+    public static final int TAG_DEST_LONGITUDE = 0x0016;
+    /** Reference for bearing of destination GPSDestBearingRef 23 17 ASCII 2 */
+    public static final int TAG_DEST_BEARING_REF = 0x0017;
+    /** Bearing of destination GPSDestBearing 24 18 RATIONAL 1 */
+    public static final int TAG_DEST_BEARING = 0x0018;
+    /** Reference for distance to destination GPSDestDistanceRef 25 19 ASCII 2 */
+    public static final int TAG_DEST_DISTANCE_REF = 0x0019;
+    /** 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. */
+    public static final int TAG_PROCESSING_METHOD = 0x001B;
+    public static final int TAG_AREA_INFORMATION = 0x001C;
+    public static final int TAG_DATE_STAMP = 0x001D;
+    public static final int TAG_DIFFERENTIAL = 0x001E;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+
+        _tagNameMap.put(TAG_VERSION_ID, "GPS Version ID");
+        _tagNameMap.put(TAG_LATITUDE_REF, "GPS Latitude Ref");
+        _tagNameMap.put(TAG_LATITUDE, "GPS Latitude");
+        _tagNameMap.put(TAG_LONGITUDE_REF, "GPS Longitude Ref");
+        _tagNameMap.put(TAG_LONGITUDE, "GPS Longitude");
+        _tagNameMap.put(TAG_ALTITUDE_REF, "GPS Altitude Ref");
+        _tagNameMap.put(TAG_ALTITUDE, "GPS Altitude");
+        _tagNameMap.put(TAG_TIME_STAMP, "GPS Time-Stamp");
+        _tagNameMap.put(TAG_SATELLITES, "GPS Satellites");
+        _tagNameMap.put(TAG_STATUS, "GPS Status");
+        _tagNameMap.put(TAG_MEASURE_MODE, "GPS Measure Mode");
+        _tagNameMap.put(TAG_DOP, "GPS DOP");
+        _tagNameMap.put(TAG_SPEED_REF, "GPS Speed Ref");
+        _tagNameMap.put(TAG_SPEED, "GPS Speed");
+        _tagNameMap.put(TAG_TRACK_REF, "GPS Track Ref");
+        _tagNameMap.put(TAG_TRACK, "GPS Track");
+        _tagNameMap.put(TAG_IMG_DIRECTION_REF, "GPS Img Direction Ref");
+        _tagNameMap.put(TAG_IMG_DIRECTION, "GPS Img Direction");
+        _tagNameMap.put(TAG_MAP_DATUM, "GPS Map Datum");
+        _tagNameMap.put(TAG_DEST_LATITUDE_REF, "GPS Dest Latitude Ref");
+        _tagNameMap.put(TAG_DEST_LATITUDE, "GPS Dest Latitude");
+        _tagNameMap.put(TAG_DEST_LONGITUDE_REF, "GPS Dest Longitude Ref");
+        _tagNameMap.put(TAG_DEST_LONGITUDE, "GPS Dest Longitude");
+        _tagNameMap.put(TAG_DEST_BEARING_REF, "GPS Dest Bearing Ref");
+        _tagNameMap.put(TAG_DEST_BEARING, "GPS Dest Bearing");
+        _tagNameMap.put(TAG_DEST_DISTANCE_REF, "GPS Dest Distance Ref");
+        _tagNameMap.put(TAG_DEST_DISTANCE, "GPS Dest Distance");
+        _tagNameMap.put(TAG_PROCESSING_METHOD, "GPS Processing Method");
+        _tagNameMap.put(TAG_AREA_INFORMATION, "GPS Area Information");
+        _tagNameMap.put(TAG_DATE_STAMP, "GPS Date Stamp");
+        _tagNameMap.put(TAG_DIFFERENTIAL, "GPS Differential");
+    }
+
+    public GpsDirectory()
+    {
+        this.setDescriptor(new GpsDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "GPS";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * Parses various tags in an attempt to obtain a single object representing the latitude and longitude
+     * at which this image was captured.
+     *
+     * @return The geographical location of this image, if possible, otherwise null
+     */
+    @Nullable
+    public GeoLocation getGeoLocation()
+    {
+        Rational[] latitudes = getRationalArray(GpsDirectory.TAG_LATITUDE);
+        Rational[] longitudes = getRationalArray(GpsDirectory.TAG_LONGITUDE);
+        String latitudeRef = getString(GpsDirectory.TAG_LATITUDE_REF);
+        String longitudeRef = getString(GpsDirectory.TAG_LONGITUDE_REF);
+
+        // Make sure we have the required values
+        if (latitudes == null || latitudes.length != 3)
+            return null;
+        if (longitudes == null || longitudes.length != 3)
+            return null;
+        if (latitudeRef == null || longitudeRef == null)
+            return null;
+
+        Double lat = GeoLocation.degreesMinutesSecondsToDecimal(latitudes[0], latitudes[1], latitudes[2], latitudeRef.equalsIgnoreCase("S"));
+        Double lon = GeoLocation.degreesMinutesSecondsToDecimal(longitudes[0], longitudes[1], longitudes[2], longitudeRef.equalsIgnoreCase("W"));
+
+        // This can return null, in cases where the conversion was not possible
+        if (lat == null || lon == null)
+            return null;
+
+        return new GeoLocation(lat, lon);
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
new file mode 100644
index 0000000..ff40300
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link CanonMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory>
+{
+    public CanonMakernoteDescriptor(@NotNull CanonMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_CANON_SERIAL_NUMBER:
+                return getSerialNumberDescription();
+            case CameraSettings.TAG_FLASH_ACTIVITY:
+                return getFlashActivityDescription();
+            case CameraSettings.TAG_FOCUS_TYPE:
+                return getFocusTypeDescription();
+            case CameraSettings.TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case CameraSettings.TAG_QUALITY:
+                return getQualityDescription();
+            case CameraSettings.TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case CameraSettings.TAG_SELF_TIMER_DELAY:
+                return getSelfTimerDelayDescription();
+            case CameraSettings.TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case CameraSettings.TAG_CONTINUOUS_DRIVE_MODE:
+                return getContinuousDriveModeDescription();
+            case CameraSettings.TAG_FOCUS_MODE_1:
+                return getFocusMode1Description();
+            case CameraSettings.TAG_IMAGE_SIZE:
+                return getImageSizeDescription();
+            case CameraSettings.TAG_EASY_SHOOTING_MODE:
+                return getEasyShootingModeDescription();
+            case CameraSettings.TAG_CONTRAST:
+                return getContrastDescription();
+            case CameraSettings.TAG_SATURATION:
+                return getSaturationDescription();
+            case CameraSettings.TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case CameraSettings.TAG_ISO:
+                return getIsoDescription();
+            case CameraSettings.TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case CameraSettings.TAG_AF_POINT_SELECTED:
+                return getAfPointSelectedDescription();
+            case CameraSettings.TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case CameraSettings.TAG_LENS_TYPE:
+                return getLensTypeDescription();
+            case CameraSettings.TAG_LONG_FOCAL_LENGTH:
+                return getLongFocalLengthDescription();
+            case CameraSettings.TAG_SHORT_FOCAL_LENGTH:
+                return getShortFocalLengthDescription();
+            case CameraSettings.TAG_FOCAL_UNITS_PER_MM:
+                return getFocalUnitsPerMillimetreDescription();
+            case CameraSettings.TAG_FLASH_DETAILS:
+                return getFlashDetailsDescription();
+            case CameraSettings.TAG_FOCUS_MODE_2:
+                return getFocusMode2Description();
+            case FocalLength.TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case FocalLength.TAG_AF_POINT_USED:
+                return getAfPointUsedDescription();
+            case FocalLength.TAG_FLASH_BIAS:
+                return getFlashBiasDescription();
+
+            // It turns out that these values are dependent upon the camera model and therefore the below code was
+            // incorrect for some Canon models.  This needs to be revisited.
+
+//            case TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION:
+//                return getLongExposureNoiseReductionDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS:
+//                return getShutterAutoExposureLockButtonDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP:
+//                return getMirrorLockupDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL:
+//                return getTvAndAvExposureLevelDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT:
+//                return getAutoFocusAssistLightDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE:
+//                return getShutterSpeedInAvModeDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_BRACKETING:
+//                return getAutoExposureBracketingSequenceAndAutoCancellationDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC:
+//                return getShutterCurtainSyncDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_AF_STOP:
+//                return getLensAutoFocusStopButtonDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION:
+//                return getFillFlashReductionDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN:
+//                return getMenuButtonReturnPositionDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION:
+//                return getSetButtonFunctionWhenShootingDescription();
+//            case TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING:
+//                return getSensorCleaningDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSerialNumberDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_SERIAL_NUMBER);
+        if (value == null)
+            return null;
+        return String.format("%04X%05d", (value >> 8) & 0xFF, value & 0xFF);
+    }
+
+/*
+    @Nullable
+    public String getLongExposureNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Off";
+            case 1:     return "On";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShutterAutoExposureLockButtonDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "AF/AE lock";
+            case 1:     return "AE lock/AF";
+            case 2:     return "AF/AF lock";
+            case 3:     return "AE+release/AE+AF";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMirrorLockupDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Disabled";
+            case 1:     return "Enabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getTvAndAvExposureLevelDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "1/2 stop";
+            case 1:     return "1/3 stop";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoFocusAssistLightDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "On (Auto)";
+            case 1:     return "Off";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShutterSpeedInAvModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Automatic";
+            case 1:     return "1/200 (fixed)";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoExposureBracketingSequenceAndAutoCancellationDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_BRACKETING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "0,-,+ / Enabled";
+            case 1:     return "0,-,+ / Disabled";
+            case 2:     return "-,0,+ / Enabled";
+            case 3:     return "-,0,+ / Disabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShutterCurtainSyncDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "1st Curtain Sync";
+            case 1:     return "2nd Curtain Sync";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getLensAutoFocusStopButtonDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_AF_STOP);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "AF stop";
+            case 1:     return "Operate AF";
+            case 2:     return "Lock AE and start timer";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFillFlashReductionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Enabled";
+            case 1:     return "Disabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMenuButtonReturnPositionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Top";
+            case 1:     return "Previous (volatile)";
+            case 2:     return "Previous";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSetButtonFunctionWhenShootingDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Not Assigned";
+            case 1:     return "Change Quality";
+            case 2:     return "Change ISO Speed";
+            case 3:     return "Select Parameters";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSensorCleaningDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:     return "Disabled";
+            case 1:     return "Enabled";
+            default:    return "Unknown (" + value + ")";
+        }
+    }
+*/
+
+    @Nullable
+    public String getFlashBiasDescription()
+    {
+        Integer value = _directory.getInteger(FocalLength.TAG_FLASH_BIAS);
+
+        if (value == null)
+            return null;
+
+        boolean isNegative = false;
+        if (value > 0xF000) {
+            isNegative = true;
+            value = 0xFFFF - value;
+            value++;
+        }
+
+        // this tag is interesting in that the values returned are:
+        //  0, 0.375, 0.5, 0.626, 1
+        // not
+        //  0, 0.33,  0.5, 0.66,  1
+
+        return ((isNegative) ? "-" : "") + Float.toString(value / 32f) + " EV";
+    }
+
+    @Nullable
+    public String getAfPointUsedDescription()
+    {
+        Integer value = _directory.getInteger(FocalLength.TAG_AF_POINT_USED);
+        if (value == null)
+            return null;
+        if ((value & 0x7) == 0) {
+            return "Right";
+        } else if ((value & 0x7) == 1) {
+            return "Centre";
+        } else if ((value & 0x7) == 2) {
+            return "Left";
+        } else {
+            return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(
+            FocalLength.TAG_WHITE_BALANCE,
+            "Auto",
+            "Sunny",
+            "Cloudy",
+            "Tungsten",
+            "Florescent",
+            "Flash",
+            "Custom"
+        );
+    }
+
+    @Nullable
+    public String getFocusMode2Description()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_MODE_2, "Single", "Continuous");
+    }
+
+    @Nullable
+    public String getFlashDetailsDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_DETAILS);
+        if (value == null)
+            return null;
+        if (((value >> 14) & 1) > 0) {
+            return "External E-TTL";
+        }
+        if (((value >> 13) & 1) > 0) {
+            return "Internal flash";
+        }
+        if (((value >> 11) & 1) > 0) {
+            return "FP sync used";
+        }
+        if (((value >> 4) & 1) > 0) {
+            return "FP sync enabled";
+        }
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getFocalUnitsPerMillimetreDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FOCAL_UNITS_PER_MM);
+        if (value == null)
+            return null;
+        if (value != 0) {
+            return Integer.toString(value);
+        } else {
+            return "";
+        }
+    }
+
+    @Nullable
+    public String getShortFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SHORT_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        String units = getFocalUnitsPerMillimetreDescription();
+        return Integer.toString(value) + " " + units;
+    }
+
+    @Nullable
+    public String getLongFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_LONG_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        String units = getFocalUnitsPerMillimetreDescription();
+        return Integer.toString(value) + " " + units;
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_EXPOSURE_MODE,
+            "Easy shooting",
+            "Program",
+            "Tv-priority",
+            "Av-priority",
+            "Manual",
+            "A-DEP"
+        );
+    }
+
+    @Nullable
+    public String getLensTypeDescription() {
+        Integer value = _directory.getInteger(CameraSettings.TAG_LENS_TYPE);
+        if (value == null)
+            return null;
+
+        return "Lens type: " + Integer.toString(value);
+    }
+
+    @Nullable
+    public String getAfPointSelectedDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_AF_POINT_SELECTED,
+            0x3000,
+            "None (MF)",
+            "Auto selected",
+            "Right",
+            "Centre",
+            "Left"
+        );
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_METERING_MODE,
+            3,
+            "Evaluative",
+            "Partial",
+            "Centre weighted"
+        );
+    }
+
+    @Nullable
+    public String getIsoDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_ISO);
+        if (value == null)
+            return null;
+
+        // Canon PowerShot S3 is special
+        int canonMask = 0x4000;
+        if ((value & canonMask) > 0)
+            return "" + (value & ~canonMask);
+
+        switch (value) {
+            case 0:
+                return "Not specified (see ISOSpeedRatings tag)";
+            case 15:
+                return "Auto";
+            case 16:
+                return "50";
+            case 17:
+                return "100";
+            case 18:
+                return "200";
+            case 19:
+                return "400";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SHARPNESS);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0xFFFF:
+                return "Low";
+            case 0x000:
+                return "Normal";
+            case 0x001:
+                return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SATURATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0xFFFF:
+                return "Low";
+            case 0x000:
+                return "Normal";
+            case 0x001:
+                return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_CONTRAST);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0xFFFF:
+                return "Low";
+            case 0x000:
+                return "Normal";
+            case 0x001:
+                return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getEasyShootingModeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_EASY_SHOOTING_MODE,
+            "Full auto",
+            "Manual",
+            "Landscape",
+            "Fast shutter",
+            "Slow shutter",
+            "Night",
+            "B&W",
+            "Sepia",
+            "Portrait",
+            "Sports",
+            "Macro / Closeup",
+            "Pan focus"
+        );
+    }
+
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_IMAGE_SIZE,
+            "Large",
+            "Medium",
+            "Small"
+        );
+    }
+
+    @Nullable
+    public String getFocusMode1Description()
+    {
+        return getIndexedDescription(
+            CameraSettings.TAG_FOCUS_MODE_1,
+            "One-shot",
+            "AI Servo",
+            "AI Focus",
+            "Manual Focus",
+            // TODO should check field 32 here (FOCUS_MODE_2)
+            "Single",
+            "Continuous",
+            "Manual Focus"
+        );
+    }
+
+    @Nullable
+    public String getContinuousDriveModeDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_CONTINUOUS_DRIVE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                final Integer delay = _directory.getInteger(CameraSettings.TAG_SELF_TIMER_DELAY);
+                if (delay != null)
+                    return delay == 0 ? "Single shot" : "Single shot with self-timer";
+            case 1:
+                return "Continuous";
+        }
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "No flash fired";
+            case 1:
+                return "Auto";
+            case 2:
+                return "On";
+            case 3:
+                return "Red-eye reduction";
+            case 4:
+                return "Slow-synchro";
+            case 5:
+                return "Auto and red-eye reduction";
+            case 6:
+                return "On and red-eye reduction";
+            case 16:
+                // note: this value not set on Canon D30
+                return "External flash";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSelfTimerDelayDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_SELF_TIMER_DELAY);
+        if (value == null)
+            return null;
+        if (value == 0) {
+            return "Self timer not used";
+        } else {
+            // TODO find an image that tests this calculation
+            return Double.toString((double)value * 0.1d) + " sec";
+        }
+    }
+
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_MACRO_MODE, 1, "Macro", "Normal");
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_QUALITY, 2, "Normal", "Fine", null, "Superfine");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "No digital zoom", "2x", "4x");
+    }
+
+    @Nullable
+    public String getFocusTypeDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_FOCUS_TYPE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Manual";
+            case 1:
+                return "Auto";
+            case 3:
+                return "Close-up (Macro)";
+            case 8:
+                return "Locked (Pan Mode)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashActivityDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FLASH_ACTIVITY, "Flash did not fire", "Flash fired");
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
new file mode 100644
index 0000000..6d31d9d
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Canon cameras.
+ *
+ * Thanks to Bill Richards for his contribution to this makernote directory.
+ *
+ * Many tag definitions explained here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CanonMakernoteDirectory extends Directory
+{
+    // These TAG_*_ARRAY Exif tags map to arrays of int16 values which are split out into separate 'fake' tags.
+    // When an attempt is made to set one of these on the directory, it is split and the corresponding offset added to the tagType.
+    // So the resulting tag is the offset + the index into the array.
+
+    private static final int TAG_CAMERA_SETTINGS_ARRAY          = 0x0001;
+    private static final int TAG_FOCAL_LENGTH_ARRAY             = 0x0002;
+//    private static final int TAG_FLASH_INFO                     = 0x0003;
+    private static final int TAG_SHOT_INFO_ARRAY                = 0x0004;
+    private static final int TAG_PANORAMA_ARRAY                 = 0x0005;
+
+    public static final int TAG_CANON_IMAGE_TYPE                = 0x0006;
+    public static final int TAG_CANON_FIRMWARE_VERSION          = 0x0007;
+    public static final int TAG_CANON_IMAGE_NUMBER              = 0x0008;
+    public static final int TAG_CANON_OWNER_NAME                = 0x0009;
+    public static final int TAG_CANON_SERIAL_NUMBER             = 0x000C;
+    public static final int TAG_CAMERA_INFO_ARRAY               = 0x000D; // depends upon model, so leave for now
+    public static final int TAG_CANON_FILE_LENGTH               = 0x000E;
+    public static final int TAG_CANON_CUSTOM_FUNCTIONS_ARRAY    = 0x000F; // depends upon model, so leave for now
+    public static final int TAG_MODEL_ID                        = 0x0010;
+    public static final int TAG_MOVIE_INFO_ARRAY                = 0x0011; // not currently decoded as not sure we see it in still images
+    private static final int TAG_AF_INFO_ARRAY                  = 0x0012; // not currently decoded
+    public static final int TAG_THUMBNAIL_IMAGE_VALID_AREA      = 0x0013;
+    public static final int TAG_SERIAL_NUMBER_FORMAT            = 0x0015;
+    public static final int TAG_SUPER_MACRO                     = 0x001A;
+    public static final int TAG_DATE_STAMP_MODE                 = 0x001C;
+    public static final int TAG_MY_COLORS                       = 0x001D;
+    public static final int TAG_FIRMWARE_REVISION               = 0x001E;
+    public static final int TAG_CATEGORIES                      = 0x0023;
+    public static final int TAG_FACE_DETECT_ARRAY_1             = 0x0024;
+    public static final int TAG_FACE_DETECT_ARRAY_2             = 0x0025;
+    public static final int TAG_AF_INFO_ARRAY_2                 = 0x0026;
+    public static final int TAG_IMAGE_UNIQUE_ID                 = 0x0028;
+
+    public static final int TAG_RAW_DATA_OFFSET                 = 0x0081;
+    public static final int TAG_ORIGINAL_DECISION_DATA_OFFSET   = 0x0083;
+
+    public static final int TAG_CUSTOM_FUNCTIONS_1D_ARRAY       = 0x0090; // not currently decoded
+    public static final int TAG_PERSONAL_FUNCTIONS_ARRAY        = 0x0091; // not currently decoded
+    public static final int TAG_PERSONAL_FUNCTION_VALUES_ARRAY  = 0x0092; // not currently decoded
+    public static final int TAG_FILE_INFO_ARRAY                 = 0x0093; // not currently decoded
+    public static final int TAG_AF_POINTS_IN_FOCUS_1D           = 0x0094;
+    public static final int TAG_LENS_MODEL                      = 0x0095;
+    public static final int TAG_SERIAL_INFO_ARRAY               = 0x0096; // not currently decoded
+    public static final int TAG_DUST_REMOVAL_DATA               = 0x0097;
+    public static final int TAG_CROP_INFO                       = 0x0098; // not currently decoded
+    public static final int TAG_CUSTOM_FUNCTIONS_ARRAY_2        = 0x0099; // not currently decoded
+    public static final int TAG_ASPECT_INFO_ARRAY               = 0x009A; // not currently decoded
+    public static final int TAG_PROCESSING_INFO_ARRAY           = 0x00A0; // not currently decoded
+    public static final int TAG_TONE_CURVE_TABLE                = 0x00A1;
+    public static final int TAG_SHARPNESS_TABLE                 = 0x00A2;
+    public static final int TAG_SHARPNESS_FREQ_TABLE            = 0x00A3;
+    public static final int TAG_WHITE_BALANCE_TABLE             = 0x00A4;
+    public static final int TAG_COLOR_BALANCE_ARRAY             = 0x00A9; // not currently decoded
+    public static final int TAG_MEASURED_COLOR_ARRAY            = 0x00AA; // not currently decoded
+    public static final int TAG_COLOR_TEMPERATURE               = 0x00AE;
+    public static final int TAG_CANON_FLAGS_ARRAY               = 0x00B0; // not currently decoded
+    public static final int TAG_MODIFIED_INFO_ARRAY             = 0x00B1; // not currently decoded
+    public static final int TAG_TONE_CURVE_MATCHING             = 0x00B2;
+    public static final int TAG_WHITE_BALANCE_MATCHING          = 0x00B3;
+    public static final int TAG_COLOR_SPACE                     = 0x00B4;
+    public static final int TAG_PREVIEW_IMAGE_INFO_ARRAY        = 0x00B6; // not currently decoded
+    public static final int TAG_VRD_OFFSET                      = 0x00D0;
+    public static final int TAG_SENSOR_INFO_ARRAY               = 0x00E0; // not currently decoded
+
+    public static final int TAG_COLOR_DATA_ARRAY_2              = 0x4001; // depends upon camera model, not currently decoded
+    public static final int TAG_CRW_PARAM                       = 0x4002; // depends upon camera model, not currently decoded
+    public static final int TAG_COLOR_INFO_ARRAY_2              = 0x4003; // not currently decoded
+    public static final int TAG_BLACK_LEVEL                     = 0x4008; // not currently decoded
+    public static final int TAG_CUSTOM_PICTURE_STYLE_FILE_NAME  = 0x4010;
+    public static final int TAG_COLOR_INFO_ARRAY                = 0x4013; // not currently decoded
+    public static final int TAG_VIGNETTING_CORRECTION_ARRAY_1   = 0x4015; // not currently decoded
+    public static final int TAG_VIGNETTING_CORRECTION_ARRAY_2   = 0x4016; // not currently decoded
+    public static final int TAG_LIGHTING_OPTIMIZER_ARRAY        = 0x4018; // not currently decoded
+    public static final int TAG_LENS_INFO_ARRAY                 = 0x4019; // not currently decoded
+    public static final int TAG_AMBIANCE_INFO_ARRAY             = 0x4020; // not currently decoded
+    public static final int TAG_FILTER_INFO_ARRAY               = 0x4024; // not currently decoded
+
+    public final static class CameraSettings
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+        private static final int OFFSET = 0xC100;
+
+        /**
+         * 1 = Macro
+         * 2 = Normal
+         */
+        public static final int TAG_MACRO_MODE = OFFSET + 0x01;
+        public static final int TAG_SELF_TIMER_DELAY = OFFSET + 0x02;
+        /**
+         * 2 = Normal
+         * 3 = Fine
+         * 5 = Superfine
+         */
+        public static final int TAG_QUALITY = OFFSET + 0x03;
+        /**
+         * 0 = Flash Not Fired
+         * 1 = Auto
+         * 2 = On
+         * 3 = Red Eye Reduction
+         * 4 = Slow Synchro
+         * 5 = Auto + Red Eye Reduction
+         * 6 = On + Red Eye Reduction
+         * 16 = External Flash
+         */
+        public static final int TAG_FLASH_MODE = OFFSET + 0x04;
+        /**
+         * 0 = Single Frame or Timer Mode
+         * 1 = Continuous
+         */
+        public static final int TAG_CONTINUOUS_DRIVE_MODE = OFFSET + 0x05;
+        public static final int TAG_UNKNOWN_2 = OFFSET + 0x06;
+        /**
+         * 0 = One-Shot
+         * 1 = AI Servo
+         * 2 = AI Focus
+         * 3 = Manual Focus
+         * 4 = Single
+         * 5 = Continuous
+         * 6 = Manual Focus
+         */
+        public static final int TAG_FOCUS_MODE_1 = OFFSET + 0x07;
+        public static final int TAG_UNKNOWN_3 = OFFSET + 0x08;
+        public static final int TAG_UNKNOWN_4 = OFFSET + 0x09;
+        /**
+         * 0 = Large
+         * 1 = Medium
+         * 2 = Small
+         */
+        public static final int TAG_IMAGE_SIZE = OFFSET + 0x0A;
+        /**
+         * 0 = Full Auto
+         * 1 = Manual
+         * 2 = Landscape
+         * 3 = Fast Shutter
+         * 4 = Slow Shutter
+         * 5 = Night
+         * 6 = Black & White
+         * 7 = Sepia
+         * 8 = Portrait
+         * 9 = Sports
+         * 10 = Macro / Close-Up
+         * 11 = Pan Focus
+         */
+        public static final int TAG_EASY_SHOOTING_MODE = OFFSET + 0x0B;
+        /**
+         * 0 = No Digital Zoom
+         * 1 = 2x
+         * 2 = 4x
+         */
+        public static final int TAG_DIGITAL_ZOOM = OFFSET + 0x0C;
+        /**
+         * 0 = Normal
+         * 1 = High
+         * 65535 = Low
+         */
+        public static final int TAG_CONTRAST = OFFSET + 0x0D;
+        /**
+         * 0 = Normal
+         * 1 = High
+         * 65535 = Low
+         */
+        public static final int TAG_SATURATION = OFFSET + 0x0E;
+        /**
+         * 0 = Normal
+         * 1 = High
+         * 65535 = Low
+         */
+        public static final int TAG_SHARPNESS = OFFSET + 0x0F;
+        /**
+         * 0 = Check ISOSpeedRatings EXIF tag for ISO Speed
+         * 15 = Auto ISO
+         * 16 = ISO 50
+         * 17 = ISO 100
+         * 18 = ISO 200
+         * 19 = ISO 400
+         */
+        public static final int TAG_ISO = OFFSET + 0x10;
+        /**
+         * 3 = Evaluative
+         * 4 = Partial
+         * 5 = Centre Weighted
+         */
+        public static final int TAG_METERING_MODE = OFFSET + 0x11;
+        /**
+         * 0 = Manual
+         * 1 = Auto
+         * 3 = Close-up (Macro)
+         * 8 = Locked (Pan Mode)
+         */
+        public static final int TAG_FOCUS_TYPE = OFFSET + 0x12;
+        /**
+         * 12288 = None (Manual Focus)
+         * 12289 = Auto Selected
+         * 12290 = Right
+         * 12291 = Centre
+         * 12292 = Left
+         */
+        public static final int TAG_AF_POINT_SELECTED = OFFSET + 0x13;
+        /**
+         * 0 = Easy Shooting (See Easy Shooting Mode)
+         * 1 = Program
+         * 2 = Tv-Priority
+         * 3 = Av-Priority
+         * 4 = Manual
+         * 5 = A-DEP
+         */
+        public static final int TAG_EXPOSURE_MODE = OFFSET + 0x14;
+        public static final int TAG_UNKNOWN_7 = OFFSET + 0x15;
+        public static final int TAG_LENS_TYPE = OFFSET + 0x16;
+        public static final int TAG_LONG_FOCAL_LENGTH = OFFSET + 0x17;
+        public static final int TAG_SHORT_FOCAL_LENGTH = OFFSET + 0x18;
+        public static final int TAG_FOCAL_UNITS_PER_MM = OFFSET + 0x19;
+        public static final int TAG_UNKNOWN_9 = OFFSET + 0x1A;
+        public static final int TAG_UNKNOWN_10 = OFFSET + 0x1B;
+        /**
+         * 0 = Flash Did Not Fire
+         * 1 = Flash Fired
+         */
+        public static final int TAG_FLASH_ACTIVITY = OFFSET + 0x1C;
+        public static final int TAG_FLASH_DETAILS = OFFSET + 0x1D;
+        public static final int TAG_UNKNOWN_12 = OFFSET + 0x1E;
+        public static final int TAG_UNKNOWN_13 = OFFSET + 0x1F;
+        /**
+         * 0 = Focus Mode: Single
+         * 1 = Focus Mode: Continuous
+         */
+        public static final int TAG_FOCUS_MODE_2 = OFFSET + 0x20;
+    }
+
+    public final static class FocalLength
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xC200;
+
+        /**
+         * 0 = Auto
+         * 1 = Sunny
+         * 2 = Cloudy
+         * 3 = Tungsten
+         * 4 = Florescent
+         * 5 = Flash
+         * 6 = Custom
+         */
+        public static final int TAG_WHITE_BALANCE = OFFSET + 0x07;
+        public static final int TAG_SEQUENCE_NUMBER = OFFSET + 0x09;
+        public static final int TAG_AF_POINT_USED = OFFSET + 0x0E;
+        /**
+         * The value of this tag may be translated into a flash bias value, in EV.
+         *
+         * 0xffc0 = -2 EV
+         * 0xffcc = -1.67 EV
+         * 0xffd0 = -1.5 EV
+         * 0xffd4 = -1.33 EV
+         * 0xffe0 = -1 EV
+         * 0xffec = -0.67 EV
+         * 0xfff0 = -0.5 EV
+         * 0xfff4 = -0.33 EV
+         * 0x0000 = 0 EV
+         * 0x000c = 0.33 EV
+         * 0x0010 = 0.5 EV
+         * 0x0014 = 0.67 EV
+         * 0x0020 = 1 EV
+         * 0x002c = 1.33 EV
+         * 0x0030 = 1.5 EV
+         * 0x0034 = 1.67 EV
+         * 0x0040 = 2 EV
+         */
+        public static final int TAG_FLASH_BIAS = OFFSET + 0x0F;
+        public static final int TAG_AUTO_EXPOSURE_BRACKETING = OFFSET + 0x10;
+        public static final int TAG_AEB_BRACKET_VALUE = OFFSET + 0x11;
+        public static final int TAG_SUBJECT_DISTANCE = OFFSET + 0x13;
+    }
+
+    public final static class ShotInfo
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xC400;
+
+        public static final int TAG_AUTO_ISO = OFFSET + 1;
+        public static final int TAG_BASE_ISO = OFFSET + 2;
+        public static final int TAG_MEASURED_EV = OFFSET + 3;
+        public static final int TAG_TARGET_APERTURE = OFFSET + 4;
+        public static final int TAG_TARGET_EXPOSURE_TIME = OFFSET + 5;
+        public static final int TAG_EXPOSURE_COMPENSATION = OFFSET + 6;
+        public static final int TAG_WHITE_BALANCE = OFFSET + 7;
+        public static final int TAG_SLOW_SHUTTER = OFFSET + 8;
+        public static final int TAG_SEQUENCE_NUMBER = OFFSET + 9;
+        public static final int TAG_OPTICAL_ZOOM_CODE = OFFSET + 10;
+        public static final int TAG_CAMERA_TEMPERATURE = OFFSET + 12;
+        public static final int TAG_FLASH_GUIDE_NUMBER = OFFSET + 13;
+        public static final int TAG_AF_POINTS_IN_FOCUS = OFFSET + 14;
+        public static final int TAG_FLASH_EXPOSURE_BRACKETING = OFFSET + 15;
+        public static final int TAG_AUTO_EXPOSURE_BRACKETING = OFFSET + 16;
+        public static final int TAG_AEB_BRACKET_VALUE = OFFSET + 17;
+        public static final int TAG_CONTROL_MODE = OFFSET + 18;
+        public static final int TAG_FOCUS_DISTANCE_UPPER = OFFSET + 19;
+        public static final int TAG_FOCUS_DISTANCE_LOWER = OFFSET + 20;
+        public static final int TAG_F_NUMBER = OFFSET + 21;
+        public static final int TAG_EXPOSURE_TIME = OFFSET + 22;
+        public static final int TAG_MEASURED_EV_2 = OFFSET + 23;
+        public static final int TAG_BULB_DURATION = OFFSET + 24;
+        public static final int TAG_CAMERA_TYPE = OFFSET + 26;
+        public static final int TAG_AUTO_ROTATE = OFFSET + 27;
+        public static final int TAG_ND_FILTER = OFFSET + 28;
+        public static final int TAG_SELF_TIMER_2 = OFFSET + 29;
+        public static final int TAG_FLASH_OUTPUT = OFFSET + 33;
+    }
+
+    public final static class Panorama
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xC500;
+
+        public static final int TAG_PANORAMA_FRAME_NUMBER = OFFSET + 2;
+        public static final int TAG_PANORAMA_DIRECTION = OFFSET + 5;
+    }
+
+    public final static class AFInfo
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xD200;
+
+        public static final int TAG_NUM_AF_POINTS = OFFSET;
+        public static final int TAG_VALID_AF_POINTS = OFFSET + 1;
+        public static final int TAG_IMAGE_WIDTH = OFFSET + 2;
+        public static final int TAG_IMAGE_HEIGHT = OFFSET + 3;
+        public static final int TAG_AF_IMAGE_WIDTH = OFFSET + 4;
+        public static final int TAG_AF_IMAGE_HEIGHT = OFFSET + 5;
+        public static final int TAG_AF_AREA_WIDTH = OFFSET + 6;
+        public static final int TAG_AF_AREA_HEIGHT = OFFSET + 7;
+        public static final int TAG_AF_AREA_X_POSITIONS = OFFSET + 8;
+        public static final int TAG_AF_AREA_Y_POSITIONS = OFFSET + 9;
+        public static final int TAG_AF_POINTS_IN_FOCUS = OFFSET + 10;
+        public static final int TAG_PRIMARY_AF_POINT_1 = OFFSET + 11;
+        public static final int TAG_PRIMARY_AF_POINT_2 = OFFSET + 12; // not sure why there are two of these
+    }
+
+//    /**
+//     * Long Exposure Noise Reduction
+//     * 0 = Off
+//     * 1 = On
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION = 0xC301;
+//
+//    /**
+//     * Shutter/Auto Exposure-lock buttons
+//     * 0 = AF/AE lock
+//     * 1 = AE lock/AF
+//     * 2 = AF/AF lock
+//     * 3 = AE+release/AE+AF
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS = 0xC302;
+//
+//    /**
+//     * Mirror lockup
+//     * 0 = Disable
+//     * 1 = Enable
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP = 0xC303;
+//
+//    /**
+//     * Tv/Av and exposure level
+//     * 0 = 1/2 stop
+//     * 1 = 1/3 stop
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL = 0xC304;
+//
+//    /**
+//     * AF-assist light
+//     * 0 = On (Auto)
+//     * 1 = Off
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT = 0xC305;
+//
+//    /**
+//     * Shutter speed in Av mode
+//     * 0 = Automatic
+//     * 1 = 1/200 (fixed)
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE = 0xC306;
+//
+//    /**
+//     * Auto-Exposure Bracketing sequence/auto cancellation
+//     * 0 = 0,-,+ / Enabled
+//     * 1 = 0,-,+ / Disabled
+//     * 2 = -,0,+ / Enabled
+//     * 3 = -,0,+ / Disabled
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_BRACKETING = 0xC307;
+//
+//    /**
+//     * Shutter Curtain Sync
+//     * 0 = 1st Curtain Sync
+//     * 1 = 2nd Curtain Sync
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC = 0xC308;
+//
+//    /**
+//     * Lens Auto-Focus stop button Function Switch
+//     * 0 = AF stop
+//     * 1 = Operate AF
+//     * 2 = Lock AE and start timer
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_AF_STOP = 0xC309;
+//
+//    /**
+//     * Auto reduction of fill flash
+//     * 0 = Enable
+//     * 1 = Disable
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION = 0xC30A;
+//
+//    /**
+//     * Menu button return position
+//     * 0 = Top
+//     * 1 = Previous (volatile)
+//     * 2 = Previous
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN = 0xC30B;
+//
+//    /**
+//     * SET button function when shooting
+//     * 0 = Not Assigned
+//     * 1 = Change Quality
+//     * 2 = Change ISO Speed
+//     * 3 = Select Parameters
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION = 0xC30C;
+//
+//    /**
+//     * Sensor cleaning
+//     * 0 = Disable
+//     * 1 = Enable
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING = 0xC30D;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CANON_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_CANON_IMAGE_NUMBER, "Image Number");
+        _tagNameMap.put(TAG_CANON_IMAGE_TYPE, "Image Type");
+        _tagNameMap.put(TAG_CANON_OWNER_NAME, "Owner Name");
+        _tagNameMap.put(TAG_CANON_SERIAL_NUMBER, "Camera Serial Number");
+        _tagNameMap.put(TAG_CAMERA_INFO_ARRAY, "Camera Info Array");
+        _tagNameMap.put(TAG_CANON_FILE_LENGTH, "File Length");
+        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTIONS_ARRAY, "Custom Functions");
+        _tagNameMap.put(TAG_MODEL_ID, "Canon Model ID");
+        _tagNameMap.put(TAG_MOVIE_INFO_ARRAY, "Movie Info Array");
+
+        _tagNameMap.put(CameraSettings.TAG_AF_POINT_SELECTED, "AF Point Selected");
+        _tagNameMap.put(CameraSettings.TAG_CONTINUOUS_DRIVE_MODE, "Continuous Drive Mode");
+        _tagNameMap.put(CameraSettings.TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(CameraSettings.TAG_EASY_SHOOTING_MODE, "Easy Shooting Mode");
+        _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_DETAILS, "Flash Details");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOCAL_UNITS_PER_MM, "Focal Units per mm");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_MODE_1, "Focus Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(CameraSettings.TAG_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(CameraSettings.TAG_ISO, "Iso");
+        _tagNameMap.put(CameraSettings.TAG_LONG_FOCAL_LENGTH, "Long Focal Length");
+        _tagNameMap.put(CameraSettings.TAG_MACRO_MODE, "Macro Mode");
+        _tagNameMap.put(CameraSettings.TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(CameraSettings.TAG_SATURATION, "Saturation");
+        _tagNameMap.put(CameraSettings.TAG_SELF_TIMER_DELAY, "Self Timer Delay");
+        _tagNameMap.put(CameraSettings.TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(CameraSettings.TAG_SHORT_FOCAL_LENGTH, "Short Focal Length");
+        _tagNameMap.put(CameraSettings.TAG_QUALITY, "Quality");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_2, "Unknown Camera Setting 2");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_3, "Unknown Camera Setting 3");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_4, "Unknown Camera Setting 4");
+        _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_TYPE, "Focus Type");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_7, "Unknown Camera Setting 7");
+        _tagNameMap.put(CameraSettings.TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_9, "Unknown Camera Setting 9");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_10, "Unknown Camera Setting 10");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_ACTIVITY, "Flash Activity");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_12, "Unknown Camera Setting 12");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_13, "Unknown Camera Setting 13");
+
+        _tagNameMap.put(FocalLength.TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(FocalLength.TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(FocalLength.TAG_AF_POINT_USED, "AF Point Used");
+        _tagNameMap.put(FocalLength.TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(FocalLength.TAG_AUTO_EXPOSURE_BRACKETING, "Auto Exposure Bracketing");
+        _tagNameMap.put(FocalLength.TAG_AEB_BRACKET_VALUE, "AEB Bracket Value");
+        _tagNameMap.put(FocalLength.TAG_SUBJECT_DISTANCE, "Subject Distance");
+
+        _tagNameMap.put(ShotInfo.TAG_AUTO_ISO, "Auto ISO");
+        _tagNameMap.put(ShotInfo.TAG_BASE_ISO, "Base ISO");
+        _tagNameMap.put(ShotInfo.TAG_MEASURED_EV, "Measured EV");
+        _tagNameMap.put(ShotInfo.TAG_TARGET_APERTURE, "Target Aperture");
+        _tagNameMap.put(ShotInfo.TAG_TARGET_EXPOSURE_TIME, "Target Exposure Time");
+        _tagNameMap.put(ShotInfo.TAG_EXPOSURE_COMPENSATION, "Exposure Compensation");
+        _tagNameMap.put(ShotInfo.TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(ShotInfo.TAG_SLOW_SHUTTER, "Slow Shutter");
+        _tagNameMap.put(ShotInfo.TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(ShotInfo.TAG_OPTICAL_ZOOM_CODE, "Optical Zoom Code");
+        _tagNameMap.put(ShotInfo.TAG_CAMERA_TEMPERATURE, "Camera Temperature");
+        _tagNameMap.put(ShotInfo.TAG_FLASH_GUIDE_NUMBER, "Flash Guide Number");
+        _tagNameMap.put(ShotInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus");
+        _tagNameMap.put(ShotInfo.TAG_FLASH_EXPOSURE_BRACKETING, "Flash Exposure Compensation");
+        _tagNameMap.put(ShotInfo.TAG_AUTO_EXPOSURE_BRACKETING, "Auto Exposure Bracketing");
+        _tagNameMap.put(ShotInfo.TAG_AEB_BRACKET_VALUE, "AEB Bracket Value");
+        _tagNameMap.put(ShotInfo.TAG_CONTROL_MODE, "Control Mode");
+        _tagNameMap.put(ShotInfo.TAG_FOCUS_DISTANCE_UPPER, "Focus Distance Upper");
+        _tagNameMap.put(ShotInfo.TAG_FOCUS_DISTANCE_LOWER, "Focus Distance Lower");
+        _tagNameMap.put(ShotInfo.TAG_F_NUMBER, "F Number");
+        _tagNameMap.put(ShotInfo.TAG_EXPOSURE_TIME, "Exposure Time");
+        _tagNameMap.put(ShotInfo.TAG_MEASURED_EV_2, "Measured EV 2");
+        _tagNameMap.put(ShotInfo.TAG_BULB_DURATION, "Bulb Duration");
+        _tagNameMap.put(ShotInfo.TAG_CAMERA_TYPE, "Camera Type");
+        _tagNameMap.put(ShotInfo.TAG_AUTO_ROTATE, "Auto Rotate");
+        _tagNameMap.put(ShotInfo.TAG_ND_FILTER, "ND Filter");
+        _tagNameMap.put(ShotInfo.TAG_SELF_TIMER_2, "Self Timer 2");
+        _tagNameMap.put(ShotInfo.TAG_FLASH_OUTPUT, "Flash Output");
+
+        _tagNameMap.put(Panorama.TAG_PANORAMA_FRAME_NUMBER, "Panorama Frame Number");
+        _tagNameMap.put(Panorama.TAG_PANORAMA_DIRECTION, "Panorama Direction");
+
+        _tagNameMap.put(AFInfo.TAG_NUM_AF_POINTS, "AF Point Count");
+        _tagNameMap.put(AFInfo.TAG_VALID_AF_POINTS, "Valid AF Point Count");
+        _tagNameMap.put(AFInfo.TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(AFInfo.TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(AFInfo.TAG_AF_IMAGE_WIDTH, "AF Image Width");
+        _tagNameMap.put(AFInfo.TAG_AF_IMAGE_HEIGHT, "AF Image Height");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_WIDTH, "AF Area Width");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_HEIGHT, "AF Area Height");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_X_POSITIONS, "AF Area X Positions");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_Y_POSITIONS, "AF Area Y Positions");
+        _tagNameMap.put(AFInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus Count");
+        _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_1, "Primary AF Point 1");
+        _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_2, "Primary AF Point 2");
+
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS, "Shutter/Auto Exposure-lock Buttons");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP, "Mirror Lockup");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL, "Tv/Av And Exposure Level");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT, "AF-Assist Light");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE, "Shutter Speed in Av Mode");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_BRACKETING, "Auto-Exposure Bracketing Sequence/Auto Cancellation");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC, "Shutter Curtain Sync");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_AF_STOP, "Lens Auto-Focus Stop Button Function Switch");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION, "Auto Reduction of Fill Flash");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN, "Menu Button Return Position");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION, "SET Button Function When Shooting");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING, "Sensor Cleaning");
+
+        _tagNameMap.put(TAG_THUMBNAIL_IMAGE_VALID_AREA, "Thumbnail Image Valid Area");
+        _tagNameMap.put(TAG_SERIAL_NUMBER_FORMAT, "Serial Number Format");
+        _tagNameMap.put(TAG_SUPER_MACRO, "Super Macro");
+        _tagNameMap.put(TAG_DATE_STAMP_MODE, "Date Stamp Mode");
+        _tagNameMap.put(TAG_MY_COLORS, "My Colors");
+        _tagNameMap.put(TAG_FIRMWARE_REVISION, "Firmware Revision");
+        _tagNameMap.put(TAG_CATEGORIES, "Categories");
+        _tagNameMap.put(TAG_FACE_DETECT_ARRAY_1, "Face Detect Array 1");
+        _tagNameMap.put(TAG_FACE_DETECT_ARRAY_2, "Face Detect Array 2");
+        _tagNameMap.put(TAG_AF_INFO_ARRAY_2, "AF Info Array 2");
+        _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Image Unique ID");
+        _tagNameMap.put(TAG_RAW_DATA_OFFSET, "Raw Data Offset");
+        _tagNameMap.put(TAG_ORIGINAL_DECISION_DATA_OFFSET, "Original Decision Data Offset");
+        _tagNameMap.put(TAG_CUSTOM_FUNCTIONS_1D_ARRAY, "Custom Functions (1D) Array");
+        _tagNameMap.put(TAG_PERSONAL_FUNCTIONS_ARRAY, "Personal Functions Array");
+        _tagNameMap.put(TAG_PERSONAL_FUNCTION_VALUES_ARRAY, "Personal Function Values Array");
+        _tagNameMap.put(TAG_FILE_INFO_ARRAY, "File Info Array");
+        _tagNameMap.put(TAG_AF_POINTS_IN_FOCUS_1D, "AF Points in Focus (1D)");
+        _tagNameMap.put(TAG_LENS_MODEL, "Lens Model");
+        _tagNameMap.put(TAG_SERIAL_INFO_ARRAY, "Serial Info Array");
+        _tagNameMap.put(TAG_DUST_REMOVAL_DATA, "Dust Removal Data");
+        _tagNameMap.put(TAG_CROP_INFO, "Crop Info");
+        _tagNameMap.put(TAG_CUSTOM_FUNCTIONS_ARRAY_2, "Custom Functions Array 2");
+        _tagNameMap.put(TAG_ASPECT_INFO_ARRAY, "Aspect Information Array");
+        _tagNameMap.put(TAG_PROCESSING_INFO_ARRAY, "Processing Information Array");
+        _tagNameMap.put(TAG_TONE_CURVE_TABLE, "Tone Curve Table");
+        _tagNameMap.put(TAG_SHARPNESS_TABLE, "Sharpness Table");
+        _tagNameMap.put(TAG_SHARPNESS_FREQ_TABLE, "Sharpness Frequency Table");
+        _tagNameMap.put(TAG_WHITE_BALANCE_TABLE, "White Balance Table");
+        _tagNameMap.put(TAG_COLOR_BALANCE_ARRAY, "Color Balance Array");
+        _tagNameMap.put(TAG_MEASURED_COLOR_ARRAY, "Measured Color Array");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_CANON_FLAGS_ARRAY, "Canon Flags Array");
+        _tagNameMap.put(TAG_MODIFIED_INFO_ARRAY, "Modified Information Array");
+        _tagNameMap.put(TAG_TONE_CURVE_MATCHING, "Tone Curve Matching");
+        _tagNameMap.put(TAG_WHITE_BALANCE_MATCHING, "White Balance Matching");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_INFO_ARRAY, "Preview Image Info Array");
+        _tagNameMap.put(TAG_VRD_OFFSET, "VRD Offset");
+        _tagNameMap.put(TAG_SENSOR_INFO_ARRAY, "Sensor Information Array");
+        _tagNameMap.put(TAG_COLOR_DATA_ARRAY_2, "Color Data Array 1");
+        _tagNameMap.put(TAG_CRW_PARAM, "CRW Parameters");
+        _tagNameMap.put(TAG_COLOR_INFO_ARRAY_2, "Color Data Array 2");
+        _tagNameMap.put(TAG_BLACK_LEVEL, "Black Level");
+        _tagNameMap.put(TAG_CUSTOM_PICTURE_STYLE_FILE_NAME, "Custom Picture Style File Name");
+        _tagNameMap.put(TAG_COLOR_INFO_ARRAY, "Color Info Array");
+        _tagNameMap.put(TAG_VIGNETTING_CORRECTION_ARRAY_1, "Vignetting Correction Array 1");
+        _tagNameMap.put(TAG_VIGNETTING_CORRECTION_ARRAY_2, "Vignetting Correction Array 2");
+        _tagNameMap.put(TAG_LIGHTING_OPTIMIZER_ARRAY, "Lighting Optimizer Array");
+        _tagNameMap.put(TAG_LENS_INFO_ARRAY, "Lens Info Array");
+        _tagNameMap.put(TAG_AMBIANCE_INFO_ARRAY, "Ambiance Info Array");
+        _tagNameMap.put(TAG_FILTER_INFO_ARRAY, "Filter Info Array");
+    }
+
+    public CanonMakernoteDirectory()
+    {
+        this.setDescriptor(new CanonMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Canon Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    @Override
+    public void setObjectArray(int tagType, @NotNull Object array)
+    {
+        // TODO is there some way to drop out 'null' or 'zero' values that are present in the array to reduce the noise?
+
+        // Certain Canon tags contain arrays of values that we split into 'fake' tags as each
+        // index in the array has its own meaning and decoding.
+        // Pick those tags out here and throw away the original array.
+        // Otherwise just add as usual.
+        switch (tagType) {
+            case TAG_CAMERA_SETTINGS_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(CameraSettings.OFFSET + i, ints[i]);
+                break;
+            }
+            case TAG_FOCAL_LENGTH_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(FocalLength.OFFSET + i, ints[i]);
+                break;
+            }
+            case TAG_SHOT_INFO_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(ShotInfo.OFFSET + i, ints[i]);
+                break;
+            }
+            case TAG_PANORAMA_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(Panorama.OFFSET + i, ints[i]);
+                break;
+            }
+            // TODO the interpretation of the custom functions tag depends upon the camera model
+//            case TAG_CANON_CUSTOM_FUNCTIONS_ARRAY:
+//                int subTagTypeBase = 0xC300;
+//                // we intentionally skip the first array member
+//                for (int i = 1; i < ints.length; i++)
+//                    setInt(subTagTypeBase + i + 1, ints[i] & 0x0F);
+//                break;
+            case TAG_AF_INFO_ARRAY: {
+                int[] ints = (int[])array;
+                for (int i = 0; i < ints.length; i++)
+                    setInt(AFInfo.OFFSET + i, ints[i]);
+                break;
+            }
+            default: {
+                // no special handling...
+                super.setObjectArray(tagType, array);
+                break;
+            }
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
new file mode 100644
index 0000000..148dd79
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.CasioType1MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link CasioType1MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory>
+{
+    public CasioType1MakernoteDescriptor(@NotNull CasioType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_RECORDING_MODE:
+                return getRecordingModeDescription();
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_FOCUSING_MODE:
+                return getFocusingModeDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FLASH_INTENSITY:
+                return getFlashIntensityDescription();
+            case TAG_OBJECT_DISTANCE:
+                return getObjectDistanceDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_CCD_SENSITIVITY:
+                return getCcdSensitivityDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getCcdSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CCD_SENSITIVITY);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            // these four for QV3000
+            case 64: return "Normal";
+            case 125: return "+1.0";
+            case 250: return "+2.0";
+            case 244: return "+3.0";
+            // these two for QV8000/2000
+            case 80: return "Normal (ISO 80 equivalent)";
+            case 100: return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal", "Soft", "Hard");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Integer value = _directory.getInteger(TAG_DIGITAL_ZOOM);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0x10000: return "No digital zoom";
+            case 0x10001: return "2x digital zoom";
+            case 0x20000: return "2x digital zoom";
+            case 0x40000: return "4x digital zoom";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 1: return "Auto";
+            case 2: return "Tungsten";
+            case 3: return "Daylight";
+            case 4: return "Florescent";
+            case 5: return "Shade";
+            case 129: return "Manual";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getObjectDistanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE);
+
+        if (value == null)
+            return null;
+
+        return value + " mm";
+    }
+
+    @Nullable
+    public String getFlashIntensityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_FLASH_INTENSITY);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 11: return "Weak";
+            case 13: return "Normal";
+            case 15: return "Strong";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE, 1, "Auto", "On", "Off", "Red eye reduction");
+    }
+
+    @Nullable
+    public String getFocusingModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUSING_MODE, 2, "Macro", "Auto focus", "Manual focus", "Infinity");
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1, "Economy", "Normal", "Fine");
+    }
+
+    @Nullable
+    public String getRecordingModeDescription()
+    {
+        return getIndexedDescription(TAG_RECORDING_MODE, 1, "Single shutter", "Panorama", "Night scene", "Portrait", "Landscape");
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
new file mode 100644
index 0000000..2c4fe85
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Casio (type 1) cameras.
+ *
+ * A standard TIFF IFD directory but always uses Motorola (Big-Endian) Byte Alignment.
+ * Makernote data begins immediately (no header).
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CasioType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_RECORDING_MODE = 0x0001;
+    public static final int TAG_QUALITY = 0x0002;
+    public static final int TAG_FOCUSING_MODE = 0x0003;
+    public static final int TAG_FLASH_MODE = 0x0004;
+    public static final int TAG_FLASH_INTENSITY = 0x0005;
+    public static final int TAG_OBJECT_DISTANCE = 0x0006;
+    public static final int TAG_WHITE_BALANCE = 0x0007;
+    public static final int TAG_UNKNOWN_1 = 0x0008;
+    public static final int TAG_UNKNOWN_2 = 0x0009;
+    public static final int TAG_DIGITAL_ZOOM = 0x000A;
+    public static final int TAG_SHARPNESS = 0x000B;
+    public static final int TAG_CONTRAST = 0x000C;
+    public static final int TAG_SATURATION = 0x000D;
+    public static final int TAG_UNKNOWN_3 = 0x000E;
+    public static final int TAG_UNKNOWN_4 = 0x000F;
+    public static final int TAG_UNKNOWN_5 = 0x0010;
+    public static final int TAG_UNKNOWN_6 = 0x0011;
+    public static final int TAG_UNKNOWN_7 = 0x0012;
+    public static final int TAG_UNKNOWN_8 = 0x0013;
+    public static final int TAG_CCD_SENSITIVITY = 0x0014;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CCD_SENSITIVITY, "CCD Sensitivity");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_FLASH_INTENSITY, "Flash Intensity");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FOCUSING_MODE, "Focusing Mode");
+        _tagNameMap.put(TAG_OBJECT_DISTANCE, "Object Distance");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_RECORDING_MODE, "Recording Mode");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_UNKNOWN_4, "Makernote Unknown 4");
+        _tagNameMap.put(TAG_UNKNOWN_5, "Makernote Unknown 5");
+        _tagNameMap.put(TAG_UNKNOWN_6, "Makernote Unknown 6");
+        _tagNameMap.put(TAG_UNKNOWN_7, "Makernote Unknown 7");
+        _tagNameMap.put(TAG_UNKNOWN_8, "Makernote Unknown 8");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+    }
+
+    public CasioType1MakernoteDirectory()
+    {
+        this.setDescriptor(new CasioType1MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Casio Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
new file mode 100644
index 0000000..4a83f0b
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.CasioType2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link CasioType2MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory>
+{
+    public CasioType2MakernoteDescriptor(@NotNull CasioType2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_THUMBNAIL_DIMENSIONS:
+                return getThumbnailDimensionsDescription();
+            case TAG_THUMBNAIL_SIZE:
+                return getThumbnailSizeDescription();
+            case TAG_THUMBNAIL_OFFSET:
+                return getThumbnailOffsetDescription();
+            case TAG_QUALITY_MODE:
+                return getQualityModeDescription();
+            case TAG_IMAGE_SIZE:
+                return getImageSizeDescription();
+            case TAG_FOCUS_MODE_1:
+                return getFocusMode1Description();
+            case TAG_ISO_SENSITIVITY:
+                return getIsoSensitivityDescription();
+            case TAG_WHITE_BALANCE_1:
+                return getWhiteBalance1Description();
+            case TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case TAG_PREVIEW_THUMBNAIL:
+                return getCasioPreviewThumbnailDescription();
+            case TAG_WHITE_BALANCE_BIAS:
+                return getWhiteBalanceBiasDescription();
+            case TAG_WHITE_BALANCE_2:
+                return getWhiteBalance2Description();
+            case TAG_OBJECT_DISTANCE:
+                return getObjectDistanceDescription();
+            case TAG_FLASH_DISTANCE:
+                return getFlashDistanceDescription();
+            case TAG_RECORD_MODE:
+                return getRecordModeDescription();
+            case TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_FOCUS_MODE_2:
+                return getFocusMode2Description();
+            case TAG_TIME_ZONE:
+                return getTimeZoneDescription();
+            case TAG_CCD_ISO_SENSITIVITY:
+                return getCcdIsoSensitivityDescription();
+            case TAG_COLOUR_MODE:
+                return getColourModeDescription();
+            case TAG_ENHANCEMENT:
+                return getEnhancementDescription();
+            case TAG_FILTER:
+                return getFilterDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFilterDescription()
+    {
+        return getIndexedDescription(TAG_FILTER, "Off");
+    }
+
+    @Nullable
+    public String getEnhancementDescription()
+    {
+        return getIndexedDescription(TAG_ENHANCEMENT, "Off");
+    }
+
+    @Nullable
+    public String getColourModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOUR_MODE, "Off");
+    }
+
+    @Nullable
+    public String getCcdIsoSensitivityDescription()
+    {
+        return getIndexedDescription(TAG_CCD_ISO_SENSITIVITY, "Off", "On");
+    }
+
+    @Nullable
+    public String getTimeZoneDescription()
+    {
+        return _directory.getString(TAG_TIME_ZONE);
+    }
+
+    @Nullable
+    public String getFocusMode2Description()
+    {
+        Integer value = _directory.getInteger(TAG_FOCUS_MODE_2);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Fixation";
+            case 6: return "Multi-Area Focus";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 3, "Fine");
+    }
+
+    @Nullable
+    public String getSelfTimerDescription()
+    {
+        return getIndexedDescription(TAG_SELF_TIMER, 1, "Off");
+    }
+
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        return getIndexedDescription(TAG_RECORD_MODE, 2, "Normal");
+    }
+
+    @Nullable
+    public String getFlashDistanceDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_DISTANCE, "Off");
+    }
+
+    @Nullable
+    public String getObjectDistanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE);
+        if (value == null)
+            return null;
+        return Integer.toString(value) + " mm";
+    }
+
+    @Nullable
+    public String getWhiteBalance2Description()
+    {
+        Integer value = _directory.getInteger(TAG_WHITE_BALANCE_2);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Manual";
+            case 1: return "Auto"; // unsure about this
+            case 4: return "Flash"; // unsure about this
+            case 12: return "Flash";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceBiasDescription()
+    {
+        return _directory.getString(TAG_WHITE_BALANCE_BIAS);
+    }
+
+    @Nullable
+    public String getCasioPreviewThumbnailDescription()
+    {
+        final byte[] bytes = _directory.getByteArray(TAG_PREVIEW_THUMBNAIL);
+        if (bytes == null)
+            return null;
+        return "<" + bytes.length + " bytes of image data>";
+    }
+
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
+    {
+        // TODO research PIM specification http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+        return _directory.getString(TAG_PRINT_IMAGE_MATCHING_INFO);
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "-1", "Normal", "+1");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "-1", "Normal", "+1");
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION, "-1", "Normal", "+1");
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Double value = _directory.getDoubleObject(TAG_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        return Double.toString(value / 10d) + " mm";
+    }
+
+    @Nullable
+    public String getWhiteBalance1Description()
+    {
+        return getIndexedDescription(
+            TAG_WHITE_BALANCE_1,
+            "Auto",
+            "Daylight",
+            "Shade",
+            "Tungsten",
+            "Florescent",
+            "Manual"
+        );
+    }
+
+    @Nullable
+    public String getIsoSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ISO_SENSITIVITY);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 3: return "50";
+            case 4: return "64";
+            case 6: return "100";
+            case 9: return "200";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFocusMode1Description()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE_1, "Normal", "Macro");
+    }
+
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_IMAGE_SIZE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "640 x 480 pixels";
+            case 4: return "1600 x 1200 pixels";
+            case 5: return "2048 x 1536 pixels";
+            case 20: return "2288 x 1712 pixels";
+            case 21: return "2592 x 1944 pixels";
+            case 22: return "2304 x 1728 pixels";
+            case 36: return "3008 x 2008 pixels";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getQualityModeDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY_MODE, 1, "Fine", "Super Fine");
+    }
+
+    @Nullable
+    public String getThumbnailOffsetDescription()
+    {
+        return _directory.getString(TAG_THUMBNAIL_OFFSET);
+    }
+
+    @Nullable
+    public String getThumbnailSizeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_THUMBNAIL_SIZE);
+        if (value == null)
+            return null;
+        return Integer.toString(value) + " bytes";
+    }
+
+    @Nullable
+    public String getThumbnailDimensionsDescription()
+    {
+        int[] dimensions = _directory.getIntArray(TAG_THUMBNAIL_DIMENSIONS);
+        if (dimensions == null || dimensions.length != 2)
+            return _directory.getString(TAG_THUMBNAIL_DIMENSIONS);
+        return dimensions[0] + " x " + dimensions[1] + " pixels";
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
new file mode 100644
index 0000000..9648e33
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Casio (type 2) cameras.
+ *
+ * A standard TIFF IFD directory but always uses Motorola (Big-Endian) Byte Alignment.
+ * Makernote data begins after a 6-byte header: "QVC\x00\x00\x00"
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class CasioType2MakernoteDirectory extends Directory
+{
+    /**
+     * 2 values - x,y dimensions in pixels.
+     */
+    public static final int TAG_THUMBNAIL_DIMENSIONS = 0x0002;
+    /**
+     * Size in bytes
+     */
+    public static final int TAG_THUMBNAIL_SIZE = 0x0003;
+    /**
+     * Offset of Preview Thumbnail
+     */
+    public static final int TAG_THUMBNAIL_OFFSET = 0x0004;
+    /**
+     * 1 = Fine
+     * 2 = Super Fine
+     */
+    public static final int TAG_QUALITY_MODE = 0x0008;
+    /**
+     * 0 = 640 x 480 pixels
+     * 4 = 1600 x 1200 pixels
+     * 5 = 2048 x 1536 pixels
+     * 20 = 2288 x 1712 pixels
+     * 21 = 2592 x 1944 pixels
+     * 22 = 2304 x 1728 pixels
+     * 36 = 3008 x 2008 pixels
+     */
+    public static final int TAG_IMAGE_SIZE = 0x0009;
+    /**
+     * 0 = Normal
+     * 1 = Macro
+     */
+    public static final int TAG_FOCUS_MODE_1 = 0x000D;
+    /**
+     * 3 = 50
+     * 4 = 64
+     * 6 = 100
+     * 9 = 200
+     */
+    public static final int TAG_ISO_SENSITIVITY = 0x0014;
+    /**
+     * 0 = Auto
+     * 1 = Daylight
+     * 2 = Shade
+     * 3 = Tungsten
+     * 4 = Fluorescent
+     * 5 = Manual
+     */
+    public static final int TAG_WHITE_BALANCE_1 = 0x0019;
+    /**
+     * Units are tenths of a millimetre
+     */
+    public static final int TAG_FOCAL_LENGTH = 0x001D;
+    /**
+     * 0 = -1
+     * 1 = Normal
+     * 2 = +1
+     */
+    public static final int TAG_SATURATION = 0x001F;
+    /**
+     * 0 = -1
+     * 1 = Normal
+     * 2 = +1
+     */
+    public static final int TAG_CONTRAST = 0x0020;
+    /**
+     * 0 = -1
+     * 1 = Normal
+     * 2 = +1
+     */
+    public static final int TAG_SHARPNESS = 0x0021;
+    /**
+     * See PIM specification here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    /**
+     * Alternate thumbnail offset
+     */
+    public static final int TAG_PREVIEW_THUMBNAIL = 0x2000;
+    /**
+     *
+     */
+    public static final int TAG_WHITE_BALANCE_BIAS = 0x2011;
+    /**
+     * 12 = Flash
+     * 0 = Manual
+     * 1 = Auto?
+     * 4 = Flash?
+     */
+    public static final int TAG_WHITE_BALANCE_2 = 0x2012;
+    /**
+     * Units are millimetres
+     */
+    public static final int TAG_OBJECT_DISTANCE = 0x2022;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_FLASH_DISTANCE = 0x2034;
+    /**
+     * 2 = Normal Mode
+     */
+    public static final int TAG_RECORD_MODE = 0x3000;
+    /**
+     * 1 = Off?
+     */
+    public static final int TAG_SELF_TIMER = 0x3001;
+    /**
+     * 3 = Fine
+     */
+    public static final int TAG_QUALITY = 0x3002;
+    /**
+     * 1 = Fixation
+     * 6 = Multi-Area Auto Focus
+     */
+    public static final int TAG_FOCUS_MODE_2 = 0x3003;
+    /**
+     * (string)
+     */
+    public static final int TAG_TIME_ZONE = 0x3006;
+    /**
+     *
+     */
+    public static final int TAG_BESTSHOT_MODE = 0x3007;
+    /**
+     * 0 = Off
+     * 1 = On?
+     */
+    public static final int TAG_CCD_ISO_SENSITIVITY = 0x3014;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_COLOUR_MODE = 0x3015;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_ENHANCEMENT = 0x3016;
+    /**
+     * 0 = Off
+     */
+    public static final int TAG_FILTER = 0x3017;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        // TODO add missing names
+        _tagNameMap.put(TAG_THUMBNAIL_DIMENSIONS, "Thumbnail Dimensions");
+        _tagNameMap.put(TAG_THUMBNAIL_SIZE, "Thumbnail Size");
+        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_QUALITY_MODE, "Quality Mode");
+        _tagNameMap.put(TAG_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(TAG_FOCUS_MODE_1, "Focus Mode");
+        _tagNameMap.put(TAG_ISO_SENSITIVITY, "ISO Sensitivity");
+        _tagNameMap.put(TAG_WHITE_BALANCE_1, "White Balance");
+        _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_PREVIEW_THUMBNAIL, "Casio Preview Thumbnail");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_WHITE_BALANCE_2, "White Balance");
+        _tagNameMap.put(TAG_OBJECT_DISTANCE, "Object Distance");
+        _tagNameMap.put(TAG_FLASH_DISTANCE, "Flash Distance");
+        _tagNameMap.put(TAG_RECORD_MODE, "Record Mode");
+        _tagNameMap.put(TAG_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(TAG_TIME_ZONE, "Time Zone");
+        _tagNameMap.put(TAG_BESTSHOT_MODE, "BestShot Mode");
+        _tagNameMap.put(TAG_CCD_ISO_SENSITIVITY, "CCD ISO Sensitivity");
+        _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_ENHANCEMENT, "Enhancement");
+        _tagNameMap.put(TAG_FILTER, "Filter");
+    }
+
+    public CasioType2MakernoteDirectory()
+    {
+        this.setDescriptor(new CasioType2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Casio Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
new file mode 100644
index 0000000..7ae3bbb
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.FujifilmMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link FujifilmMakernoteDirectory}.
+ * <p>
+ * Fujifilm added their Makernote tag from the Year 2000's models (e.g.Finepix1400,
+ * Finepix4700). It uses IFD format and start from ASCII character 'FUJIFILM', and next 4
+ * bytes (value 0x000c) points the offset to first IFD entry.
+ * <pre><code>
+ * :0000: 46 55 4A 49 46 49 4C 4D-0C 00 00 00 0F 00 00 00 :0000: FUJIFILM........
+ * :0010: 07 00 04 00 00 00 30 31-33 30 00 10 02 00 08 00 :0010: ......0130......
+ * </code></pre>
+ * There are two big differences to the other manufacturers.
+ * <ul>
+ * <li>Fujifilm's Exif data uses Motorola align, but Makernote ignores it and uses Intel align.</li>
+ * <li>
+ * The other manufacturer's Makernote counts the "offset to data" from the first byte of TIFF header
+ * (same as the other IFD), but Fujifilm counts it from the first byte of Makernote itself.
+ * </li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FujifilmMakernoteDescriptor extends TagDescriptor<FujifilmMakernoteDirectory>
+{
+    public FujifilmMakernoteDescriptor(@NotNull FujifilmMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_COLOR_SATURATION:
+                return getColorSaturationDescription();
+            case TAG_TONE:
+                return getToneDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_NOISE_REDUCTION:
+                return getNoiseReductionDescription();
+            case TAG_HIGH_ISO_NOISE_REDUCTION:
+                return getHighIsoNoiseReductionDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FLASH_EV:
+                return getFlashExposureValueDescription();
+            case TAG_MACRO:
+                return getMacroDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_SLOW_SYNC:
+                return getSlowSyncDescription();
+            case TAG_PICTURE_MODE:
+                return getPictureModeDescription();
+            case TAG_EXR_AUTO:
+                return getExrAutoDescription();
+            case TAG_EXR_MODE:
+                return getExrModeDescription();
+            case TAG_AUTO_BRACKETING:
+                return getAutoBracketingDescription();
+            case TAG_FINE_PIX_COLOR:
+                return getFinePixColorDescription();
+            case TAG_BLUR_WARNING:
+                return getBlurWarningDescription();
+            case TAG_FOCUS_WARNING:
+                return getFocusWarningDescription();
+            case TAG_AUTO_EXPOSURE_WARNING:
+                return getAutoExposureWarningDescription();
+            case TAG_DYNAMIC_RANGE:
+                return getDynamicRangeDescription();
+            case TAG_FILM_MODE:
+                return getFilmModeDescription();
+            case TAG_DYNAMIC_RANGE_SETTING:
+                return getDynamicRangeSettingDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_SHARPNESS);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Softest";
+            case 2: return "Soft";
+            case 3: return "Normal";
+            case 4: return "Hard";
+            case 5: return "Hardest";
+            case 0x82: return "Medium Soft";
+            case 0x84: return "Medium Hard";
+            case 0x8000: return "Film Simulation";
+            case 0xFFFF: return "N/A";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Auto";
+            case 0x100: return "Daylight";
+            case 0x200: return "Cloudy";
+            case 0x300: return "Daylight Fluorescent";
+            case 0x301: return "Day White Fluorescent";
+            case 0x302: return "White Fluorescent";
+            case 0x303: return "Warm White Fluorescent";
+            case 0x304: return "Living Room Warm White Fluorescent";
+            case 0x400: return "Incandescence";
+            case 0x500: return "Flash";
+            case 0xf00: return "Custom White Balance";
+            case 0xf01: return "Custom White Balance 2";
+            case 0xf02: return "Custom White Balance 3";
+            case 0xf03: return "Custom White Balance 4";
+            case 0xf04: return "Custom White Balance 5";
+            case 0xff0: return "Kelvin";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorSaturationDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_COLOR_SATURATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x080: return "Medium High";
+            case 0x100: return "High";
+            case 0x180: return "Medium Low";
+            case 0x200: return "Low";
+            case 0x300: return "None (B&W)";
+            case 0x301: return "B&W Green Filter";
+            case 0x302: return "B&W Yellow Filter";
+            case 0x303: return "B&W Blue Filter";
+            case 0x304: return "B&W Sepia";
+            case 0x8000: return "Film Simulation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getToneDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_TONE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x080: return "Medium High";
+            case 0x100: return "High";
+            case 0x180: return "Medium Low";
+            case 0x200: return "Low";
+            case 0x300: return "None (B&W)";
+            case 0x8000: return "Film Simulation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_CONTRAST);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x100: return "High";
+            case 0x300: return "Low";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNoiseReductionDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x040: return "Low";
+            case 0x080: return "Normal";
+            case 0x100: return "N/A";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getHighIsoNoiseReductionDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_HIGH_ISO_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Normal";
+            case 0x100: return "Strong";
+            case 0x200: return "Weak";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(
+            TAG_FLASH_MODE,
+            "Auto",
+            "On",
+            "Off",
+            "Red-eye Reduction",
+            "External"
+        );
+    }
+
+    @Nullable
+    public String getFlashExposureValueDescription()
+    {
+        Rational value = _directory.getRational(TAG_FLASH_EV);
+        return value == null ? null : value.toSimpleString(false) + " EV (Apex)";
+    }
+
+    @Nullable
+    public String getMacroDescription()
+    {
+        return getIndexedDescription(TAG_MACRO, "Off", "On");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, "Auto Focus", "Manual Focus");
+    }
+
+    @Nullable
+    public String getSlowSyncDescription()
+    {
+        return getIndexedDescription(TAG_SLOW_SYNC, "Off", "On");
+    }
+
+    @Nullable
+    public String getPictureModeDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_PICTURE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Auto";
+            case 0x001: return "Portrait scene";
+            case 0x002: return "Landscape scene";
+            case 0x003: return "Macro";
+            case 0x004: return "Sports scene";
+            case 0x005: return "Night scene";
+            case 0x006: return "Program AE";
+            case 0x007: return "Natural Light";
+            case 0x008: return "Anti-blur";
+            case 0x009: return "Beach & Snow";
+            case 0x00a: return "Sunset";
+            case 0x00b: return "Museum";
+            case 0x00c: return "Party";
+            case 0x00d: return "Flower";
+            case 0x00e: return "Text";
+            case 0x00f: return "Natural Light & Flash";
+            case 0x010: return "Beach";
+            case 0x011: return "Snow";
+            case 0x012: return "Fireworks";
+            case 0x013: return "Underwater";
+            case 0x014: return "Portrait with Skin Correction";
+            // skip 0x015
+            case 0x016: return "Panorama";
+            case 0x017: return "Night (Tripod)";
+            case 0x018: return "Pro Low-light";
+            case 0x019: return "Pro Focus";
+            // skip 0x01a
+            case 0x01b: return "Dog Face Detection";
+            case 0x01c: return "Cat Face Detection";
+            case 0x100: return "Aperture priority AE";
+            case 0x200: return "Shutter priority AE";
+            case 0x300: return "Manual exposure";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getExrAutoDescription()
+    {
+        return getIndexedDescription(TAG_EXR_AUTO, "Auto", "Manual");
+    }
+
+    @Nullable
+    public String getExrModeDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXR_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x100: return "HR (High Resolution)";
+            case 0x200: return "SN (Signal to Noise Priority)";
+            case 0x300: return "DR (Dynamic Range Priority)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoBracketingDescription()
+    {
+        return getIndexedDescription(
+            TAG_AUTO_BRACKETING,
+            "Off",
+            "On",
+            "No Flash & Flash"
+        );
+    }
+
+    @Nullable
+    public String getFinePixColorDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_FINE_PIX_COLOR);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "Standard";
+            case 0x10: return "Chrome";
+            case 0x30: return "B&W";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getBlurWarningDescription()
+    {
+        return getIndexedDescription(
+            TAG_BLUR_WARNING,
+            "No Blur Warning",
+            "Blur warning"
+        );
+    }
+
+    @Nullable
+    public String getFocusWarningDescription()
+    {
+        return getIndexedDescription(
+            TAG_FOCUS_WARNING,
+            "Good Focus",
+            "Out Of Focus"
+        );
+    }
+
+    @Nullable
+    public String getAutoExposureWarningDescription()
+    {
+        return getIndexedDescription(
+            TAG_AUTO_EXPOSURE_WARNING,
+            "AE Good",
+            "Over Exposed"
+        );
+    }
+
+    @Nullable
+    public String getDynamicRangeDescription()
+    {
+        return getIndexedDescription(
+            TAG_DYNAMIC_RANGE,
+            1,
+            "Standard",
+            null,
+            "Wide"
+        );
+    }
+
+    @Nullable
+    public String getFilmModeDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_FILM_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "F0/Standard (Provia) ";
+            case 0x100: return "F1/Studio Portrait";
+            case 0x110: return "F1a/Studio Portrait Enhanced Saturation";
+            case 0x120: return "F1b/Studio Portrait Smooth Skin Tone (Astia)";
+            case 0x130: return "F1c/Studio Portrait Increased Sharpness";
+            case 0x200: return "F2/Fujichrome (Velvia)";
+            case 0x300: return "F3/Studio Portrait Ex";
+            case 0x400: return "F4/Velvia";
+            case 0x500: return "Pro Neg. Std";
+            case 0x501: return "Pro Neg. Hi";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getDynamicRangeSettingDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_DYNAMIC_RANGE_SETTING);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x000: return "Auto (100-400%)";
+            case 0x001: return "Manual";
+            case 0x100: return "Standard (100%)";
+            case 0x200: return "Wide 1 (230%)";
+            case 0x201: return "Wide 2 (400%)";
+            case 0x8000: return "Film Simulation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
new file mode 100644
index 0000000..a5aa44e
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Fujifilm cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FujifilmMakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_VERSION = 0x0000;
+    public static final int TAG_SERIAL_NUMBER = 0x0010;
+
+    public static final int TAG_QUALITY = 0x1000;
+    public static final int TAG_SHARPNESS = 0x1001;
+    public static final int TAG_WHITE_BALANCE = 0x1002;
+    public static final int TAG_COLOR_SATURATION = 0x1003;
+    public static final int TAG_TONE = 0x1004;
+    public static final int TAG_COLOR_TEMPERATURE = 0x1005;
+    public static final int TAG_CONTRAST = 0x1006;
+
+    public static final int TAG_WHITE_BALANCE_FINE_TUNE = 0x100a;
+    public static final int TAG_NOISE_REDUCTION = 0x100b;
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION = 0x100e;
+
+    public static final int TAG_FLASH_MODE = 0x1010;
+    public static final int TAG_FLASH_EV = 0x1011;
+
+    public static final int TAG_MACRO = 0x1020;
+    public static final int TAG_FOCUS_MODE = 0x1021;
+    public static final int TAG_FOCUS_PIXEL = 0x1023;
+
+    public static final int TAG_SLOW_SYNC = 0x1030;
+    public static final int TAG_PICTURE_MODE = 0x1031;
+    public static final int TAG_EXR_AUTO = 0x1033;
+    public static final int TAG_EXR_MODE = 0x1034;
+
+    public static final int TAG_AUTO_BRACKETING = 0x1100;
+    public static final int TAG_SEQUENCE_NUMBER = 0x1101;
+
+    public static final int TAG_FINE_PIX_COLOR = 0x1210;
+
+    public static final int TAG_BLUR_WARNING = 0x1300;
+    public static final int TAG_FOCUS_WARNING = 0x1301;
+    public static final int TAG_AUTO_EXPOSURE_WARNING = 0x1302;
+    public static final int TAG_GE_IMAGE_SIZE = 0x1304;
+
+    public static final int TAG_DYNAMIC_RANGE = 0x1400;
+    public static final int TAG_FILM_MODE = 0x1401;
+    public static final int TAG_DYNAMIC_RANGE_SETTING = 0x1402;
+    public static final int TAG_DEVELOPMENT_DYNAMIC_RANGE = 0x1403;
+    public static final int TAG_MIN_FOCAL_LENGTH = 0x1404;
+    public static final int TAG_MAX_FOCAL_LENGTH = 0x1405;
+    public static final int TAG_MAX_APERTURE_AT_MIN_FOCAL = 0x1406;
+    public static final int TAG_MAX_APERTURE_AT_MAX_FOCAL = 0x1407;
+
+    public static final int TAG_AUTO_DYNAMIC_RANGE = 0x140b;
+
+    public static final int TAG_FACES_DETECTED = 0x4100;
+    /**
+     * Left, top, right and bottom coordinates in full-sized image for each face detected.
+     */
+    public static final int TAG_FACE_POSITIONS = 0x4103;
+    public static final int TAG_FACE_REC_INFO = 0x4282;
+
+    public static final int TAG_FILE_SOURCE = 0x8000;
+    public static final int TAG_ORDER_NUMBER = 0x8002;
+    public static final int TAG_FRAME_NUMBER = 0x8003;
+
+    public static final int TAG_PARALLAX = 0xb211;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_COLOR_SATURATION, "Color Saturation");
+        _tagNameMap.put(TAG_TONE, "Tone (Contrast)");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+
+        _tagNameMap.put(TAG_WHITE_BALANCE_FINE_TUNE, "White Balance Fine Tune");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FLASH_EV, "Flash Strength");
+
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FOCUS_PIXEL, "Focus Pixel");
+
+        _tagNameMap.put(TAG_SLOW_SYNC, "Slow Sync");
+        _tagNameMap.put(TAG_PICTURE_MODE, "Picture Mode");
+        _tagNameMap.put(TAG_EXR_AUTO, "EXR Auto");
+        _tagNameMap.put(TAG_EXR_MODE, "EXR Mode");
+
+        _tagNameMap.put(TAG_AUTO_BRACKETING, "Auto Bracketing");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+
+        _tagNameMap.put(TAG_FINE_PIX_COLOR, "FinePix Color Setting");
+
+        _tagNameMap.put(TAG_BLUR_WARNING, "Blur Warning");
+        _tagNameMap.put(TAG_FOCUS_WARNING, "Focus Warning");
+        _tagNameMap.put(TAG_AUTO_EXPOSURE_WARNING, "AE Warning");
+        _tagNameMap.put(TAG_GE_IMAGE_SIZE, "GE Image Size");
+
+        _tagNameMap.put(TAG_DYNAMIC_RANGE, "Dynamic Range");
+        _tagNameMap.put(TAG_FILM_MODE, "Film Mode");
+        _tagNameMap.put(TAG_DYNAMIC_RANGE_SETTING, "Dynamic Range Setting");
+        _tagNameMap.put(TAG_DEVELOPMENT_DYNAMIC_RANGE, "Development Dynamic Range");
+        _tagNameMap.put(TAG_MIN_FOCAL_LENGTH, "Minimum Focal Length");
+        _tagNameMap.put(TAG_MAX_FOCAL_LENGTH, "Maximum Focal Length");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MIN_FOCAL, "Maximum Aperture at Minimum Focal Length");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MAX_FOCAL, "Maximum Aperture at Maximum Focal Length");
+
+        _tagNameMap.put(TAG_AUTO_DYNAMIC_RANGE, "Auto Dynamic Range");
+
+        _tagNameMap.put(TAG_FACES_DETECTED, "Faces Detected");
+        _tagNameMap.put(TAG_FACE_POSITIONS, "Face Positions");
+        _tagNameMap.put(TAG_FACE_REC_INFO, "Face Detection Data");
+
+        _tagNameMap.put(TAG_FILE_SOURCE, "File Source");
+        _tagNameMap.put(TAG_ORDER_NUMBER, "Order Number");
+        _tagNameMap.put(TAG_FRAME_NUMBER, "Frame Number");
+
+        _tagNameMap.put(TAG_PARALLAX, "Parallax");
+    }
+
+    public FujifilmMakernoteDirectory()
+    {
+        this.setDescriptor(new FujifilmMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Fujifilm Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
new file mode 100644
index 0000000..8f2c1aa
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.KodakMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link KodakMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory>
+{
+    public KodakMakernoteDescriptor(@NotNull KodakMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_BURST_MODE:
+                return getBurstModeDescription();
+            case TAG_SHUTTER_MODE:
+                return getShutterModeDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FLASH_FIRED:
+                return getFlashFiredDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal");
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_COLOR_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x001: case 0x2000: return "B&W";
+            case 0x002: case 0x4000: return "Sepia";
+            case 0x003: return "B&W Yellow Filter";
+            case 0x004: return "B&W Red Filter";
+            case 0x020: return "Saturated Color";
+            case 0x040: case 0x200: return "Neutral Color";
+            case 0x100: return "Saturated Color";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashFiredDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_FIRED, "No", "Yes");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_FLASH_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "Auto";
+            case 0x10: case 0x01: return "Fill Flash";
+            case 0x20: case 0x02: return "Off";
+            case 0x40: case 0x03: return "Red Eye";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE, "Auto", "Flash", "Tungsten", "Daylight");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, "Normal", null, "Macro");
+    }
+
+    @Nullable
+    public String getShutterModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SHUTTER_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Auto";
+            case 8: return "Aperture Priority";
+            case 32: return "Manual";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getBurstModeDescription()
+    {
+        return getIndexedDescription(TAG_BURST_MODE, "Off", "On");
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1, "Fine", "Normal");
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
new file mode 100644
index 0000000..b60edc0
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Kodak cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KodakMakernoteDirectory extends Directory
+{
+    public final static int TAG_KODAK_MODEL = 0;
+    public final static int TAG_QUALITY = 9;
+    public final static int TAG_BURST_MODE = 10;
+    public final static int TAG_IMAGE_WIDTH = 12;
+    public final static int TAG_IMAGE_HEIGHT = 14;
+    public final static int TAG_YEAR_CREATED = 16;
+    public final static int TAG_MONTH_DAY_CREATED = 18;
+    public final static int TAG_TIME_CREATED = 20;
+    public final static int TAG_BURST_MODE_2 = 24;
+    public final static int TAG_SHUTTER_MODE = 27;
+    public final static int TAG_METERING_MODE = 28;
+    public final static int TAG_SEQUENCE_NUMBER = 29;
+    public final static int TAG_F_NUMBER = 30;
+    public final static int TAG_EXPOSURE_TIME = 32;
+    public final static int TAG_EXPOSURE_COMPENSATION = 36;
+    public final static int TAG_FOCUS_MODE = 56;
+    public final static int TAG_WHITE_BALANCE = 64;
+    public final static int TAG_FLASH_MODE = 92;
+    public final static int TAG_FLASH_FIRED = 93;
+    public final static int TAG_ISO_SETTING = 94;
+    public final static int TAG_ISO = 96;
+    public final static int TAG_TOTAL_ZOOM = 98;
+    public final static int TAG_DATE_TIME_STAMP = 100;
+    public final static int TAG_COLOR_MODE = 102;
+    public final static int TAG_DIGITAL_ZOOM = 104;
+    public final static int TAG_SHARPNESS = 107;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_KODAK_MODEL, "Kodak Model");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_BURST_MODE, "Burst Mode");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_YEAR_CREATED, "Year Created");
+        _tagNameMap.put(TAG_MONTH_DAY_CREATED, "Month/Day Created");
+        _tagNameMap.put(TAG_TIME_CREATED, "Time Created");
+        _tagNameMap.put(TAG_BURST_MODE_2, "Burst Mode 2");
+        _tagNameMap.put(TAG_SHUTTER_MODE, "Shutter Speed");
+        _tagNameMap.put(TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(TAG_F_NUMBER, "F Number");
+        _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time");
+        _tagNameMap.put(TAG_EXPOSURE_COMPENSATION, "Exposure Compensation");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FLASH_FIRED, "Flash Fired");
+        _tagNameMap.put(TAG_ISO_SETTING, "ISO Setting");
+        _tagNameMap.put(TAG_ISO, "ISO");
+        _tagNameMap.put(TAG_TOTAL_ZOOM, "Total Zoom");
+        _tagNameMap.put(TAG_DATE_TIME_STAMP, "Date/Time Stamp");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+    }
+
+    public KodakMakernoteDirectory()
+    {
+        this.setDescriptor(new KodakMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Kodak Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
new file mode 100644
index 0000000..c5a2935
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.KyoceraMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link KyoceraMakernoteDirectory}.
+ * <p>
+ * Some information about this makernote taken from here:
+ * http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
+ * <p>
+ * Most manufacturer's Makernote counts the "offset to data" from the first byte
+ * of TIFF header (same as the other IFD), but Kyocera (along with Fujifilm) counts
+ * it from the first byte of Makernote itself.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDirectory>
+{
+    public KyoceraMakernoteDescriptor(@NotNull KyoceraMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case TAG_PROPRIETARY_THUMBNAIL:
+                return getProprietaryThumbnailDataDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
+    {
+        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
+    }
+
+    @Nullable
+    public String getProprietaryThumbnailDataDescription()
+    {
+        return getByteLengthDescription(TAG_PROPRIETARY_THUMBNAIL);
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
new file mode 100644
index 0000000..7139f9c
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Kyocera and Contax cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class KyoceraMakernoteDirectory extends Directory
+{
+    public static final int TAG_PROPRIETARY_THUMBNAIL = 0x0001;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_PROPRIETARY_THUMBNAIL, "Proprietary Thumbnail Format Data");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+    }
+
+    public KyoceraMakernoteDirectory()
+    {
+        this.setDescriptor(new KyoceraMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Kyocera/Contax Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
new file mode 100644
index 0000000..1fb494c
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.LeicaMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link LeicaMakernoteDirectory}.
+ * <p>
+ * Tag reference from: http://gvsoft.homedns.org/exif/makernote-leica-type1.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class LeicaMakernoteDescriptor extends TagDescriptor<LeicaMakernoteDirectory>
+{
+    public LeicaMakernoteDescriptor(@NotNull LeicaMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_USER_PROFILE:
+                return getUserProfileDescription();
+//            case TAG_SERIAL:
+//                return getSerialNumberDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE:
+                return getExternalSensorBrightnessValueDescription();
+            case TAG_MEASURED_LV:
+                return getMeasuredLvDescription();
+            case TAG_APPROXIMATE_F_NUMBER:
+                return getApproximateFNumberDescription();
+            case TAG_CAMERA_TEMPERATURE:
+                return getCameraTemperatureDescription();
+            case TAG_WB_RED_LEVEL:
+            case TAG_WB_BLUE_LEVEL:
+            case TAG_WB_GREEN_LEVEL:
+                return getSimpleRational(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getCameraTemperatureDescription()
+    {
+        return getFormattedInt(TAG_CAMERA_TEMPERATURE, "%d C");
+    }
+
+    @Nullable
+    private String getApproximateFNumberDescription()
+    {
+        return getSimpleRational(TAG_APPROXIMATE_F_NUMBER);
+    }
+
+    @Nullable
+    private String getMeasuredLvDescription()
+    {
+        return getSimpleRational(TAG_MEASURED_LV);
+    }
+
+    @Nullable
+    private String getExternalSensorBrightnessValueDescription()
+    {
+        return getSimpleRational(TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE);
+    }
+
+    @Nullable
+    private String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            "Auto or Manual",
+            "Daylight",
+            "Fluorescent",
+            "Tungsten",
+            "Flash",
+            "Cloudy",
+            "Shadow"
+        );
+    }
+
+    @Nullable
+    private String getUserProfileDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1,
+            "User Profile 1",
+            "User Profile 2",
+            "User Profile 3",
+            "User Profile 0 (Dynamic)"
+        );
+    }
+
+    @Nullable
+    private String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY, 1, "Fine", "Basic");
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
new file mode 100644
index 0000000..a2e6c38
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to certain Leica cameras.
+ * <p>
+ * Tag reference from: http://gvsoft.homedns.org/exif/makernote-leica-type1.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class LeicaMakernoteDirectory extends Directory
+{
+    public static final int TAG_QUALITY = 0x0300;
+    public static final int TAG_USER_PROFILE = 0x0302;
+    public static final int TAG_SERIAL_NUMBER = 0x0303;
+    public static final int TAG_WHITE_BALANCE = 0x0304;
+
+    public static final int TAG_LENS_TYPE = 0x0310;
+    public static final int TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE = 0x0311;
+    public static final int TAG_MEASURED_LV = 0x0312;
+    public static final int TAG_APPROXIMATE_F_NUMBER = 0x0313;
+
+    public static final int TAG_CAMERA_TEMPERATURE = 0x0320;
+    public static final int TAG_COLOR_TEMPERATURE = 0x0321;
+    public static final int TAG_WB_RED_LEVEL = 0x0322;
+    public static final int TAG_WB_GREEN_LEVEL = 0x0323;
+    public static final int TAG_WB_BLUE_LEVEL = 0x0324;
+
+    public static final int TAG_CCD_VERSION = 0x0330;
+    public static final int TAG_CCD_BOARD_VERSION = 0x0331;
+    public static final int TAG_CONTROLLER_BOARD_VERSION = 0x0332;
+    public static final int TAG_M16_C_VERSION = 0x0333;
+
+    public static final int TAG_IMAGE_ID_NUMBER = 0x0340;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_USER_PROFILE, "User Profile");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_EXTERNAL_SENSOR_BRIGHTNESS_VALUE, "External Sensor Brightness Value");
+        _tagNameMap.put(TAG_MEASURED_LV, "Measured LV");
+        _tagNameMap.put(TAG_APPROXIMATE_F_NUMBER, "Approximate F Number");
+
+        _tagNameMap.put(TAG_CAMERA_TEMPERATURE, "Camera Temperature");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_WB_RED_LEVEL, "WB Red Level");
+        _tagNameMap.put(TAG_WB_GREEN_LEVEL, "WB Green Level");
+        _tagNameMap.put(TAG_WB_BLUE_LEVEL, "WB Blue Level");
+
+        _tagNameMap.put(TAG_CCD_VERSION, "CCD Version");
+        _tagNameMap.put(TAG_CCD_BOARD_VERSION, "CCD Board Version");
+        _tagNameMap.put(TAG_CONTROLLER_BOARD_VERSION, "Controller Board Version");
+        _tagNameMap.put(TAG_M16_C_VERSION, "M16 C Version");
+
+        _tagNameMap.put(TAG_IMAGE_ID_NUMBER, "Image ID Number");
+    }
+
+    public LeicaMakernoteDirectory()
+    {
+        this.setDescriptor(new LeicaMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Leica Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
new file mode 100644
index 0000000..3df1370
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.NikonType1MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link NikonType1MakernoteDirectory}.
+ * <p>
+ * Type-1 is for E-Series cameras prior to (not including) E990.  For example: E700, E800, E900,
+ * E900S, E910, E950.
+ * <p>
+ * Makernote starts from ASCII string "Nikon". Data format is the same as IFD, but it starts from
+ * offset 0x08. This is the same as Olympus except start string. Example of actual data
+ * structure is shown below.
+ * <pre><code>
+ * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
+ * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
+ * </code></pre>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType1MakernoteDescriptor extends TagDescriptor<NikonType1MakernoteDirectory>
+{
+    public NikonType1MakernoteDescriptor(@NotNull NikonType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY:
+                return getQualityDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_IMAGE_ADJUSTMENT:
+                return getImageAdjustmentDescription();
+            case TAG_CCD_SENSITIVITY:
+                return getCcdSensitivityDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FOCUS:
+                return getFocusDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_CONVERTER:
+                return getConverterDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getConverterDescription()
+    {
+        return getIndexedDescription(TAG_CONVERTER, "None", "Fisheye converter");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+                ? "No digital zoom"
+                : value.toSimpleString(true) + "x digital zoom";
+    }
+
+    @Nullable
+    public String getFocusDescription()
+    {
+        Rational value = _directory.getRational(TAG_FOCUS);
+        return value == null
+            ? null
+            : value.getNumerator() == 1 && value.getDenominator() == 0
+                ? "Infinite"
+                : value.toSimpleString(true);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            "Auto",
+            "Preset",
+            "Daylight",
+            "Incandescence",
+            "Florescence",
+            "Cloudy",
+            "SpeedLight"
+        );
+    }
+
+    @Nullable
+    public String getCcdSensitivityDescription()
+    {
+        return getIndexedDescription(TAG_CCD_SENSITIVITY,
+            "ISO80",
+            null,
+            "ISO160",
+            null,
+            "ISO320",
+            "ISO100"
+        );
+    }
+
+    @Nullable
+    public String getImageAdjustmentDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_ADJUSTMENT,
+            "Normal",
+            "Bright +",
+            "Bright -",
+            "Contrast +",
+            "Contrast -"
+        );
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_MODE,
+            1,
+            "Color",
+            "Monochrome"
+        );
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY,
+            1,
+            "VGA Basic",
+            "VGA Normal",
+            "VGA Fine",
+            "SXGA Basic",
+            "SXGA Normal",
+            "SXGA Fine"
+        );
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
new file mode 100644
index 0000000..ad7ac57
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Nikon (type 1) cameras.  Type-1 is for E-Series cameras prior to (not including) E990.
+ *
+ * There are 3 formats of Nikon's Makernote. Makernote of E700/E800/E900/E900S/E910/E950
+ * starts from ASCII string "Nikon". Data format is the same as IFD, but it starts from
+ * offset 0x08. This is the same as Olympus except start string. Example of actual data
+ * structure is shown below.
+ * <pre><code>
+ * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........
+ * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
+ * </code></pre>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_UNKNOWN_1 = 0x0002;
+    public static final int TAG_QUALITY = 0x0003;
+    public static final int TAG_COLOR_MODE = 0x0004;
+    public static final int TAG_IMAGE_ADJUSTMENT = 0x0005;
+    public static final int TAG_CCD_SENSITIVITY = 0x0006;
+    public static final int TAG_WHITE_BALANCE = 0x0007;
+    public static final int TAG_FOCUS = 0x0008;
+    public static final int TAG_UNKNOWN_2 = 0x0009;
+    public static final int TAG_DIGITAL_ZOOM = 0x000A;
+    public static final int TAG_CONVERTER = 0x000B;
+    public static final int TAG_UNKNOWN_3 = 0x0F00;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CCD_SENSITIVITY, "CCD Sensitivity");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_CONVERTER, "Fisheye Converter");
+        _tagNameMap.put(TAG_FOCUS, "Focus");
+        _tagNameMap.put(TAG_IMAGE_ADJUSTMENT, "Image Adjustment");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+    }
+
+    public NikonType1MakernoteDirectory()
+    {
+        this.setDescriptor(new NikonType1MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Nikon Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
new file mode 100644
index 0000000..a55a911
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DecimalFormat;
+
+import static com.drew.metadata.exif.makernotes.NikonType2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link NikonType2MakernoteDirectory}.
+ *
+ * Type-2 applies to the E990 and D-series cameras such as the D1, D70 and D100.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2MakernoteDirectory>
+{
+    public NikonType2MakernoteDescriptor(@NotNull NikonType2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType)
+        {
+            case TAG_PROGRAM_SHIFT:
+                return getProgramShiftDescription();
+            case TAG_EXPOSURE_DIFFERENCE:
+                return getExposureDifferenceDescription();
+            case TAG_LENS:
+                return getLensDescription();
+            case TAG_CAMERA_HUE_ADJUSTMENT:
+                return getHueAdjustmentDescription();
+            case TAG_CAMERA_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_AUTO_FLASH_COMPENSATION:
+                return getAutoFlashCompensationDescription();
+            case TAG_FLASH_EXPOSURE_COMPENSATION:
+                return getFlashExposureCompensationDescription();
+            case TAG_FLASH_BRACKET_COMPENSATION:
+                return getFlashBracketCompensationDescription();
+            case TAG_EXPOSURE_TUNING:
+                return getExposureTuningDescription();
+            case TAG_LENS_STOPS:
+                return getLensStopsDescription();
+            case TAG_COLOR_SPACE:
+                return getColorSpaceDescription();
+            case TAG_ACTIVE_D_LIGHTING:
+                return getActiveDLightingDescription();
+            case TAG_VIGNETTE_CONTROL:
+                return getVignetteControlDescription();
+            case TAG_ISO_1:
+                return getIsoSettingDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_FLASH_USED:
+                return getFlashUsedDescription();
+            case TAG_AF_FOCUS_POSITION:
+                return getAutoFocusPositionDescription();
+            case TAG_FIRMWARE_VERSION:
+                return getFirmwareVersionDescription();
+            case TAG_LENS_TYPE:
+                return getLensTypeDescription();
+            case TAG_SHOOTING_MODE:
+                return getShootingModeDescription();
+            case TAG_NEF_COMPRESSION:
+                return getNEFCompressionDescription();
+            case TAG_HIGH_ISO_NOISE_REDUCTION:
+                return getHighISONoiseReductionDescription();
+            case TAG_POWER_UP_TIME:
+                return getPowerUpTimeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPowerUpTimeDescription()
+    {
+        return getEpochTimeDescription(TAG_POWER_UP_TIME);
+    }
+
+    @Nullable
+    public String getHighISONoiseReductionDescription()
+    {
+        return getIndexedDescription(TAG_HIGH_ISO_NOISE_REDUCTION,
+            "Off",
+            "Minimal",
+            "Low",
+            null,
+            "Normal",
+            null,
+            "High"
+        );
+    }
+
+    @Nullable
+    public String getFlashUsedDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_USED,
+            "Flash Not Used",
+            "Manual Flash",
+            null,
+            "Flash Not Ready",
+            null,
+            null,
+            null,
+            "External Flash",
+            "Fired, Commander Mode",
+            "Fired, TTL Mode"
+        );
+    }
+
+    @Nullable
+    public String getNEFCompressionDescription()
+    {
+        return getIndexedDescription(TAG_NEF_COMPRESSION,
+            1,
+            "Lossy (Type 1)",
+            null,
+            "Uncompressed",
+            null,
+            null,
+            null,
+            "Lossless",
+            "Lossy (Type 2)"
+        );
+    }
+
+    @Nullable
+    public String getShootingModeDescription()
+    {
+        return getBitFlagDescription(TAG_SHOOTING_MODE,
+            // LSB [low label, high label]
+            new String[]{"Single Frame", "Continuous"},
+            "Delay",
+            null,
+            "PC Control",
+            "Exposure Bracketing",
+            "Auto ISO",
+            "White-Balance Bracketing",
+            "IR Control"
+        );
+    }
+
+    @Nullable
+    public String getLensTypeDescription()
+    {
+        return getBitFlagDescription(TAG_LENS_TYPE,
+            // LSB [low label, high label]
+            new String[]{"AF", "MF"},
+            "D",
+            "G",
+            "VR"
+        );
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_SPACE,
+            1,
+            "sRGB",
+            "Adobe RGB"
+        );
+    }
+
+    @Nullable
+    public String getActiveDLightingDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACTIVE_D_LIGHTING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Light";
+            case 3: return "Normal";
+            case 5: return "High";
+            case 7: return "Extra High";
+            case 65535: return "Auto";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getVignetteControlDescription()
+    {
+        Integer value = _directory.getInteger(TAG_VIGNETTE_CONTROL);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Low";
+            case 3: return "Normal";
+            case 5: return "High";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoFocusPositionDescription()
+    {
+        int[] values = _directory.getIntArray(TAG_AF_FOCUS_POSITION);
+        if (values==null)
+            return null;
+        if (values.length != 4 || values[0] != 0 || values[2] != 0 || values[3] != 0) {
+            return "Unknown (" + _directory.getString(TAG_AF_FOCUS_POSITION) + ")";
+        }
+        switch (values[1]) {
+            case 0:
+                return "Centre";
+            case 1:
+                return "Top";
+            case 2:
+                return "Bottom";
+            case 3:
+                return "Left";
+            case 4:
+                return "Right";
+            default:
+                return "Unknown (" + values[1] + ")";
+        }
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM);
+        if (value==null)
+            return null;
+        return value.intValue() == 1
+                ? "No digital zoom"
+                : value.toSimpleString(true) + "x digital zoom";
+    }
+
+    @Nullable
+    public String getProgramShiftDescription()
+    {
+        return getEVDescription(TAG_PROGRAM_SHIFT);
+    }
+
+    @Nullable
+    public String getExposureDifferenceDescription()
+    {
+        return getEVDescription(TAG_EXPOSURE_DIFFERENCE);
+    }
+
+    @Nullable
+    public String getAutoFlashCompensationDescription()
+    {
+        return getEVDescription(TAG_AUTO_FLASH_COMPENSATION);
+    }
+
+    @Nullable
+    public String getFlashExposureCompensationDescription()
+    {
+        return getEVDescription(TAG_FLASH_EXPOSURE_COMPENSATION);
+    }
+
+    @Nullable
+    public String getFlashBracketCompensationDescription()
+    {
+        return getEVDescription(TAG_FLASH_BRACKET_COMPENSATION);
+    }
+
+    @Nullable
+    public String getExposureTuningDescription()
+    {
+        return getEVDescription(TAG_EXPOSURE_TUNING);
+    }
+
+    @Nullable
+    public String getLensStopsDescription()
+    {
+        return getEVDescription(TAG_LENS_STOPS);
+    }
+
+    @Nullable
+    private String getEVDescription(int tagType)
+    {
+        int[] values = _directory.getIntArray(tagType);
+        if (values == null)
+            return null;
+        if (values.length < 3 || values[2] == 0)
+            return null;
+        final DecimalFormat decimalFormat = new DecimalFormat("0.##");
+        double ev = values[0] * values[1] / (double)values[2];
+        return decimalFormat.format(ev) + " EV";
+    }
+
+    @Nullable
+    public String getIsoSettingDescription()
+    {
+        int[] values = _directory.getIntArray(TAG_ISO_1);
+        if (values == null)
+            return null;
+        if (values[0] != 0 || values[1] == 0)
+            return "Unknown (" + _directory.getString(TAG_ISO_1) + ")";
+        return "ISO " + values[1];
+    }
+
+    @Nullable
+    public String getLensDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TAG_LENS);
+
+        return values == null
+            ? null
+            : values.length < 4
+                ? _directory.getString(TAG_LENS)
+                : String.format("%d-%dmm f/%.1f-%.1f", values[0].intValue(), values[1].intValue(), values[2].floatValue(), values[3].floatValue());
+
+    }
+
+    @Nullable
+    public String getHueAdjustmentDescription()
+    {
+        return getFormattedString(TAG_CAMERA_HUE_ADJUSTMENT, "%s degrees");
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        String value = _directory.getString(TAG_CAMERA_COLOR_MODE);
+        return value == null ? null : value.startsWith("MODE1") ? "Mode I (sRGB)" : value;
+    }
+
+    @Nullable
+    public String getFirmwareVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FIRMWARE_VERSION, 2);
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
new file mode 100644
index 0000000..39249a1
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
@@ -0,0 +1,924 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Nikon (type 2) cameras.  Type-2 applies to the E990 and D-series cameras such as the E990, D1,
+ * D70 and D100.
+ * <p>
+ * Thanks to Fabrizio Giudici for publishing his reverse-engineering of the D100 makernote data.
+ * http://www.timelesswanderings.net/equipment/D100/NEF.html
+ * <p>
+ * Note that the camera implements image protection (locking images) via the file's 'readonly' attribute.  Similarly
+ * image hiding uses the 'hidden' attribute (observed on the D70).  Consequently, these values are not available here.
+ * <p>
+ * Additional sample images have been observed, and their tag values recorded in javadoc comments for each tag's field.
+ * New tags have subsequently been added since Fabrizio's observations.
+ * <p>
+ * In earlier models (such as the E990 and D1), this directory begins at the first byte of the makernote IFD.  In
+ * later models, the IFD was given the standard prefix to indicate the camera models (most other manufacturers also
+ * provide this prefix to aid in software decoding).
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class NikonType2MakernoteDirectory extends Directory
+{
+    /**
+     * Values observed
+     * - 0200 (D70)
+     * - 0200 (D1X)
+     */
+    public static final int TAG_FIRMWARE_VERSION = 0x0001;
+
+    /**
+     * Values observed
+     * - 0 250
+     * - 0 400
+     */
+    public static final int TAG_ISO_1 = 0x0002;
+
+    /**
+     * The camera's color mode, as an uppercase string.  Examples include:
+     * <ul>
+     * <li><code>B & W</code></li>
+     * <li><code>COLOR</code></li>
+     * <li><code>COOL</code></li>
+     * <li><code>SEPIA</code></li>
+     * <li><code>VIVID</code></li>
+     * </ul>
+     */
+    public static final int TAG_COLOR_MODE = 0x0003;
+
+    /**
+     * The camera's quality setting, as an uppercase string.  Examples include:
+     * <ul>
+     * <li><code>BASIC</code></li>
+     * <li><code>FINE</code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>RAW</code></li>
+     * <li><code>RAW2.7M</code></li>
+     * </ul>
+     */
+    public static final int TAG_QUALITY_AND_FILE_FORMAT = 0x0004;
+
+    /**
+     * The camera's white balance setting, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>CLOUDY</code></li>
+     * <li><code>FLASH</code></li>
+     * <li><code>FLUORESCENT</code></li>
+     * <li><code>INCANDESCENT</code></li>
+     * <li><code>PRESET</code></li>
+     * <li><code>PRESET0</code></li>
+     * <li><code>PRESET1</code></li>
+     * <li><code>PRESET3</code></li>
+     * <li><code>SUNNY</code></li>
+     * <li><code>WHITE PRESET</code></li>
+     * <li><code>4350K</code></li>
+     * <li><code>5000K</code></li>
+     * <li><code>DAY WHITE FL</code></li>
+     * <li><code>SHADE</code></li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_WHITE_BALANCE  = 0x0005;
+
+    /**
+     * The camera's sharpening setting, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>HIGH</code></li>
+     * <li><code>LOW</code></li>
+     * <li><code>NONE</code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>MED.H</code></li>
+     * <li><code>MED.L</code></li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_SHARPENING = 0x0006;
+
+    /**
+     * The camera's auto-focus mode, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code>AF-C</code></li>
+     * <li><code>AF-S</code></li>
+     * <li><code>MANUAL</code></li>
+     * <li><code>AF-A</code></li>
+     * </ul>
+     */
+    public static final int TAG_AF_TYPE = 0x0007;
+
+    /**
+     * The camera's flash setting, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>RED-EYE</code></li>
+     * <li><code>SLOW</code></li>
+     * <li><code>NEW_TTL</code></li>
+     * <li><code>REAR</code></li>
+     * <li><code>REAR SLOW</code></li>
+     * </ul>
+     * Note: when TAG_AUTO_FLASH_MODE is blank (whitespace), Nikon Browser displays "Flash Sync Mode: Not Attached"
+     */
+    public static final int TAG_FLASH_SYNC_MODE = 0x0008;
+
+    /**
+     * The type of flash used in the photograph, as a string.  Examples include:
+     *
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>Built-in,TTL</code></li>
+     * <li><code>NEW_TTL</code> Nikon Browser interprets as "D-TTL"</li>
+     * <li><code>Built-in,M</code></li>
+     * <li><code>Optional,TTL</code> with speedlight SB800, flash sync mode as "NORMAL"</li>
+     * </ul>
+     */
+    public static final int TAG_AUTO_FLASH_MODE = 0x0009;
+
+    /**
+     * An unknown tag, as a rational.  Several values given here:
+     * http://gvsoft.homedns.org/exif/makernote-nikon-type2.html#0x000b
+     */
+    public static final int TAG_UNKNOWN_34 = 0x000A;
+
+    /**
+     * The camera's white balance bias setting, as an uint16 array having either one or two elements.
+     *
+     * <ul>
+     * <li><code>0</code></li>
+     * <li><code>1</code></li>
+     * <li><code>-3</code></li>
+     * <li><code>-2</code></li>
+     * <li><code>-1</code></li>
+     * <li><code>0,0</code></li>
+     * <li><code>1,0</code></li>
+     * <li><code>5,-5</code></li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_WHITE_BALANCE_FINE = 0x000B;
+
+    /**
+     * The first two numbers are coefficients to multiply red and blue channels according to white balance as set in the
+     * camera. The meaning of the third and the fourth numbers is unknown.
+     *
+     * Values observed
+     * - 2.25882352 1.76078431 0.0 0.0
+     * - 10242/1 34305/1 0/1 0/1
+     * - 234765625/100000000 1140625/1000000 1/1 1/1
+     */
+    public static final int TAG_CAMERA_WHITE_BALANCE_RB_COEFF = 0x000C;
+
+    /**
+     * The camera's program shift setting, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>0,1,3,0</code> = 0 EV</li>
+     * <li><code>1,1,3,0</code> = 0.33 EV</li>
+     * <li><code>-3,1,3,0</code> = -1 EV</li>
+     * <li><code>1,1,2,0</code> = 0.5 EV</li>
+     * <li><code>2,1,6,0</code> = 0.33 EV</li>
+     * </ul>
+     */
+    public static final int TAG_PROGRAM_SHIFT = 0x000D;
+
+    /**
+     * The exposure difference, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>-105,1,12,0</code> = -8.75 EV</li>
+     * <li><code>-72,1,12,0</code> = -6.00 EV</li>
+     * <li><code>-11,1,12,0</code> = -0.92 EV</li>
+     * </ul>
+     */
+    public static final int TAG_EXPOSURE_DIFFERENCE = 0x000E;
+
+    /**
+     * The camera's ISO mode, as an uppercase string.
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>MANUAL</code></li>
+     * </ul>
+     */
+    public static final int TAG_ISO_MODE = 0x000F;
+
+    /**
+     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
+     */
+    public static final int TAG_DATA_DUMP = 0x0010;
+
+    /**
+     * Preview to another IFD (?)
+     * <p>
+     * Details here: http://gvsoft.homedns.org/exif/makernote-nikon-2-tag0x0011.html
+     * // TODO if this is another IFD, decode it
+     */
+    public static final int TAG_PREVIEW_IFD = 0x0011;
+
+    /**
+     * The flash compensation, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>-18,1,6,0</code> = -3 EV</li>
+     * <li><code>4,1,6,0</code> = 0.67 EV</li>
+     * <li><code>6,1,6,0</code> = 1 EV</li>
+     * </ul>
+     */
+    public static final int TAG_AUTO_FLASH_COMPENSATION = 0x0012;
+
+    /**
+     * The requested ISO value, as an array of two integers.
+     *
+     * <ul>
+     * <li><code>0,0</code></li>
+     * <li><code>0,125</code></li>
+     * <li><code>1,2500</code></li>
+     * </ul>
+     */
+    public static final int TAG_ISO_REQUESTED = 0x0013;
+
+    /**
+     * Defines the photo corner coordinates, in 8 bytes.  Treated as four 16-bit integers, they
+     * decode as: top-left (x,y); bot-right (x,y)
+     * - 0 0 49163 53255
+     * - 0 0 3008 2000 (the image dimensions were 3008x2000) (D70)
+     * <ul>
+     * <li><code>0,0,4288,2848</code> The max resolution of the D300 camera</li>
+     * <li><code>0,0,3008,2000</code> The max resolution of the D70 camera</li>
+     * <li><code>0,0,4256,2832</code> The max resolution of the D3 camera</li>
+     * </ul>
+     */
+    public static final int TAG_IMAGE_BOUNDARY = 0x0016;
+
+    /**
+     * The flash exposure compensation, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>0,0,0,0</code> = 0 EV</li>
+     * <li><code>0,1,6,0</code> = 0 EV</li>
+     * <li><code>4,1,6,0</code> = 0.67 EV</li>
+     * </ul>
+     */
+    public static final int TAG_FLASH_EXPOSURE_COMPENSATION = 0x0017;
+
+    /**
+     * The flash bracket compensation, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>0,0,0,0</code> = 0 EV</li>
+     * <li><code>0,1,6,0</code> = 0 EV</li>
+     * <li><code>4,1,6,0</code> = 0.67 EV</li>
+     * </ul>
+     */
+    public static final int TAG_FLASH_BRACKET_COMPENSATION = 0x0018;
+
+    /**
+     * The AE bracket compensation, as a rational number.
+     *
+     * <ul>
+     * <li><code>0/0</code></li>
+     * <li><code>0/1</code></li>
+     * <li><code>0/6</code></li>
+     * <li><code>4/6</code></li>
+     * <li><code>6/6</code></li>
+     * </ul>
+     */
+    public static final int TAG_AE_BRACKET_COMPENSATION = 0x0019;
+
+    /**
+     * Flash mode, as a string.
+     *
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>Red Eye Reduction</code></li>
+     * <li><code>D-Lighting</code></li>
+     * <li><code>Distortion control</code></li>
+     * </ul>
+     */
+    public static final int TAG_FLASH_MODE = 0x001a;
+
+    public static final int TAG_CROP_HIGH_SPEED = 0x001b;
+    public static final int TAG_EXPOSURE_TUNING = 0x001c;
+
+    /**
+     * The camera's serial number, as a string.
+     * Note that D200 is always blank, and D50 is always <code>"D50"</code>.
+     */
+    public static final int TAG_CAMERA_SERIAL_NUMBER = 0x001d;
+
+    /**
+     * The camera's color space setting.
+     *
+     * <ul>
+     * <li><code>1</code> sRGB</li>
+     * <li><code>2</code> Adobe RGB</li>
+     * </ul>
+     */
+    public static final int TAG_COLOR_SPACE = 0x001e;
+    public static final int TAG_VR_INFO = 0x001f;
+    public static final int TAG_IMAGE_AUTHENTICATION = 0x0020;
+    public static final int TAG_UNKNOWN_35 = 0x0021;
+
+    /**
+     * The active D-Lighting setting.
+     *
+     * <ul>
+     * <li><code>0</code> Off</li>
+     * <li><code>1</code> Low</li>
+     * <li><code>3</code> Normal</li>
+     * <li><code>5</code> High</li>
+     * <li><code>7</code> Extra High</li>
+     * <li><code>65535</code> Auto</li>
+     * </ul>
+     */
+    public static final int TAG_ACTIVE_D_LIGHTING = 0x0022;
+    public static final int TAG_PICTURE_CONTROL = 0x0023;
+    public static final int TAG_WORLD_TIME = 0x0024;
+    public static final int TAG_ISO_INFO = 0x0025;
+    public static final int TAG_UNKNOWN_36 = 0x0026;
+    public static final int TAG_UNKNOWN_37 = 0x0027;
+    public static final int TAG_UNKNOWN_38 = 0x0028;
+    public static final int TAG_UNKNOWN_39 = 0x0029;
+
+    /**
+     * The camera's vignette control setting.
+     *
+     * <ul>
+     * <li><code>0</code> Off</li>
+     * <li><code>1</code> Low</li>
+     * <li><code>3</code> Normal</li>
+     * <li><code>5</code> High</li>
+     * </ul>
+     */
+    public static final int TAG_VIGNETTE_CONTROL = 0x002a;
+    public static final int TAG_UNKNOWN_40 = 0x002b;
+    public static final int TAG_UNKNOWN_41 = 0x002c;
+    public static final int TAG_UNKNOWN_42 = 0x002d;
+    public static final int TAG_UNKNOWN_43 = 0x002e;
+    public static final int TAG_UNKNOWN_44 = 0x002f;
+    public static final int TAG_UNKNOWN_45 = 0x0030;
+    public static final int TAG_UNKNOWN_46 = 0x0031;
+
+    /**
+     * The camera's image adjustment setting, as a string.
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>CONTRAST(+)</code></li>
+     * <li><code>CONTRAST(-)</code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>B & W</code></li>
+     * <li><code>BRIGHTNESS(+)</code></li>
+     * <li><code>BRIGHTNESS(-)</code></li>
+     * <li><code>SEPIA</code></li>
+     * </ul>
+     */
+    public static final int TAG_IMAGE_ADJUSTMENT = 0x0080;
+
+    /**
+     * The camera's tone compensation setting, as a string.
+     *
+     * <ul>
+     * <li><code>NORMAL</code></li>
+     * <li><code>LOW</code></li>
+     * <li><code>MED.L</code></li>
+     * <li><code>MED.H</code></li>
+     * <li><code>HIGH</code></li>
+     * <li><code>AUTO</code></li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_TONE_COMPENSATION = 0x0081;
+
+    /**
+     * A description of any auxiliary lens, as a string.
+     *
+     * <ul>
+     * <li><code>OFF</code></li>
+     * <li><code>FISHEYE 1</code></li>
+     * <li><code>FISHEYE 2</code></li>
+     * <li><code>TELEPHOTO 2</code></li>
+     * <li><code>WIDE ADAPTER</code></li>
+     * </ul>
+     */
+    public static final int TAG_ADAPTER = 0x0082;
+
+    /**
+     * The type of lens used, as a byte.
+     *
+     * <ul>
+     * <li><code>0x00</code> AF</li>
+     * <li><code>0x01</code> MF</li>
+     * <li><code>0x02</code> D</li>
+     * <li><code>0x06</code> G, D</li>
+     * <li><code>0x08</code> VR</li>
+     * <li><code>0x0a</code> VR, D</li>
+     * <li><code>0x0e</code> VR, G, D</li>
+     * </ul>
+     */
+    public static final int TAG_LENS_TYPE = 0x0083;
+
+    /**
+     * A pair of focal/max-fstop values that describe the lens used.
+     *
+     * Values observed
+     * - 180.0,180.0,2.8,2.8 (D100)
+     * - 240/10 850/10 35/10 45/10
+     * - 18-70mm f/3.5-4.5 (D70)
+     * - 17-35mm f/2.8-2.8 (D1X)
+     * - 70-200mm f/2.8-2.8 (D70)
+     *
+     * Nikon Browser identifies the lens as "18-70mm F/3.5-4.5 G" which
+     * is identical to metadata extractor, except for the "G".  This must
+     * be coming from another tag...
+     */
+    public static final int TAG_LENS = 0x0084;
+
+    /**
+     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
+     */
+    public static final int TAG_MANUAL_FOCUS_DISTANCE = 0x0085;
+
+    /**
+     * The amount of digital zoom used.
+     */
+    public static final int TAG_DIGITAL_ZOOM = 0x0086;
+
+    /**
+     * Whether the flash was used in this image.
+     *
+     * <ul>
+     * <li><code>0</code> Flash Not Used</li>
+     * <li><code>1</code> Manual Flash</li>
+     * <li><code>3</code> Flash Not Ready</li>
+     * <li><code>7</code> External Flash</li>
+     * <li><code>8</code> Fired, Commander Mode</li>
+     * <li><code>9</code> Fired, TTL Mode</li>
+     * </ul>
+     */
+    public static final int TAG_FLASH_USED = 0x0087;
+
+    /**
+     * The position of the autofocus target.
+     */
+    public static final int TAG_AF_FOCUS_POSITION = 0x0088;
+
+    /**
+     * The camera's shooting mode.
+     * <p>
+     * A bit-array with:
+     * <ul>
+     * <li><code>0</code> Single Frame</li>
+     * <li><code>1</code> Continuous</li>
+     * <li><code>2</code> Delay</li>
+     * <li><code>8</code> PC Control</li>
+     * <li><code>16</code> Exposure Bracketing</li>
+     * <li><code>32</code> Auto ISO</li>
+     * <li><code>64</code> White-Balance Bracketing</li>
+     * <li><code>128</code> IR Control</li>
+     * </ul>
+     */
+    public static final int TAG_SHOOTING_MODE = 0x0089;
+
+    public static final int TAG_UNKNOWN_20 = 0x008A;
+
+    /**
+     * Lens stops, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>64,1,12,0</code> = 5.33 EV</li>
+     * <li><code>72,1,12,0</code> = 6 EV</li>
+     * </ul>
+     */
+    public static final int TAG_LENS_STOPS = 0x008B;
+
+    public static final int TAG_CONTRAST_CURVE = 0x008C;
+
+    /**
+     * The color space as set in the camera, as a string.
+     *
+     * <ul>
+     * <li><code>MODE1</code> = Mode 1 (sRGB)</li>
+     * <li><code>MODE1a</code> = Mode 1 (sRGB)</li>
+     * <li><code>MODE2</code> = Mode 2 (Adobe RGB)</li>
+     * <li><code>MODE3</code> = Mode 2 (sRGB): Higher Saturation</li>
+     * <li><code>MODE3a</code> = Mode 2 (sRGB): Higher Saturation</li>
+     * <li><code>B & W</code> = B & W</li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_COLOR_MODE = 0x008D;
+    public static final int TAG_UNKNOWN_47 = 0x008E;
+
+    /**
+     * The camera's scene mode, as a string.  Examples include:
+     * <ul>
+     * <li><code>BEACH/SNOW</code></li>
+     * <li><code>CLOSE UP</code></li>
+     * <li><code>NIGHT PORTRAIT</code></li>
+     * <li><code>PORTRAIT</code></li>
+     * <li><code>ANTI-SHAKE</code></li>
+     * <li><code>BACK LIGHT</code></li>
+     * <li><code>BEST FACE</code></li>
+     * <li><code>BEST</code></li>
+     * <li><code>COPY</code></li>
+     * <li><code>DAWN/DUSK</code></li>
+     * <li><code>FACE-PRIORITY</code></li>
+     * <li><code>FIREWORKS</code></li>
+     * <li><code>FOOD</code></li>
+     * <li><code>HIGH SENS.</code></li>
+     * <li><code>LAND SCAPE</code></li>
+     * <li><code>MUSEUM</code></li>
+     * <li><code>PANORAMA ASSIST</code></li>
+     * <li><code>PARTY/INDOOR</code></li>
+     * <li><code>SCENE AUTO</code></li>
+     * <li><code>SMILE</code></li>
+     * <li><code>SPORT</code></li>
+     * <li><code>SPORT CONT.</code></li>
+     * <li><code>SUNSET</code></li>
+     * </ul>
+     */
+    public static final int TAG_SCENE_MODE = 0x008F;
+
+    /**
+     * The lighting type, as a string.  Examples include:
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>NATURAL</code></li>
+     * <li><code>SPEEDLIGHT</code></li>
+     * <li><code>COLORED</code></li>
+     * <li><code>MIXED</code></li>
+     * <li><code>NORMAL</code></li>
+     * </ul>
+     */
+    public static final int TAG_LIGHT_SOURCE = 0x0090;
+
+    /**
+     * Advertised as ASCII, but actually isn't.  A variable number of bytes (eg. 18 to 533).  Actual number of bytes
+     * appears fixed for a given camera model.
+     */
+    public static final int TAG_SHOT_INFO = 0x0091;
+
+    /**
+     * The hue adjustment as set in the camera.  Values observed are either 0 or 3.
+     */
+    public static final int TAG_CAMERA_HUE_ADJUSTMENT = 0x0092;
+    /**
+     * The NEF (RAW) compression.  Examples include:
+     * <ul>
+     * <li><code>1</code> Lossy (Type 1)</li>
+     * <li><code>2</code> Uncompressed</li>
+     * <li><code>3</code> Lossless</li>
+     * <li><code>4</code> Lossy (Type 2)</li>
+     * </ul>
+     */
+    public static final int TAG_NEF_COMPRESSION = 0x0093;
+
+    /**
+     * The saturation level, as a signed integer.  Examples include:
+     * <ul>
+     * <li><code>+3</code></li>
+     * <li><code>+2</code></li>
+     * <li><code>+1</code></li>
+     * <li><code>0</code> Normal</li>
+     * <li><code>-1</code></li>
+     * <li><code>-2</code></li>
+     * <li><code>-3</code> (B&W)</li>
+     * </ul>
+     */
+    public static final int TAG_SATURATION = 0x0094;
+
+    /**
+     * The type of noise reduction, as a string.  Examples include:
+     * <ul>
+     * <li><code>OFF</code></li>
+     * <li><code>FPNR</code></li>
+     * </ul>
+     */
+    public static final int TAG_NOISE_REDUCTION = 0x0095;
+    public static final int TAG_LINEARIZATION_TABLE = 0x0096;
+    public static final int TAG_COLOR_BALANCE = 0x0097;
+    public static final int TAG_LENS_DATA = 0x0098;
+
+    /** The NEF (RAW) thumbnail size, as an integer array with two items representing [width,height]. */
+    public static final int TAG_NEF_THUMBNAIL_SIZE = 0x0099;
+
+    /** The sensor pixel size, as a pair of rational numbers. */
+    public static final int TAG_SENSOR_PIXEL_SIZE = 0x009A;
+    public static final int TAG_UNKNOWN_10 = 0x009B;
+    public static final int TAG_SCENE_ASSIST = 0x009C;
+    public static final int TAG_UNKNOWN_11 = 0x009D;
+    public static final int TAG_RETOUCH_HISTORY = 0x009E;
+    public static final int TAG_UNKNOWN_12 = 0x009F;
+
+    /**
+     * The camera serial number, as a string.
+     * <ul>
+     * <li><code>NO= 00002539</code></li>
+     * <li><code>NO= -1000d71</code></li>
+     * <li><code>PKG597230621263</code></li>
+     * <li><code>PKG5995671330625116</code></li>
+     * <li><code>PKG49981281631130677</code></li>
+     * <li><code>BU672230725063</code></li>
+     * <li><code>NO= 200332c7</code></li>
+     * <li><code>NO= 30045efe</code></li>
+     * </ul>
+     */
+    public static final int TAG_CAMERA_SERIAL_NUMBER_2 = 0x00A0;
+
+    public static final int TAG_IMAGE_DATA_SIZE = 0x00A2;
+
+    public static final int TAG_UNKNOWN_27 = 0x00A3;
+    public static final int TAG_UNKNOWN_28 = 0x00A4;
+    public static final int TAG_IMAGE_COUNT = 0x00A5;
+    public static final int TAG_DELETED_IMAGE_COUNT = 0x00A6;
+
+    /** The number of total shutter releases.  This value increments for each exposure (observed on D70). */
+    public static final int TAG_EXPOSURE_SEQUENCE_NUMBER = 0x00A7;
+
+    public static final int TAG_FLASH_INFO = 0x00A8;
+    /**
+     * The camera's image optimisation, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>NORMAL</code></li>
+     *     <li><code>CUSTOM</code></li>
+     *     <li><code>BLACK AND WHITE</code></li>
+     *     <li><code>LAND SCAPE</code></li>
+     *     <li><code>MORE VIVID</code></li>
+     *     <li><code>PORTRAIT</code></li>
+     *     <li><code>SOFT</code></li>
+     *     <li><code>VIVID</code></li>
+     * </ul>
+     */
+    public static final int TAG_IMAGE_OPTIMISATION = 0x00A9;
+
+    /**
+     * The camera's saturation level, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>NORMAL</code></li>
+     *     <li><code>AUTO</code></li>
+     *     <li><code>ENHANCED</code></li>
+     *     <li><code>MODERATE</code></li>
+     * </ul>
+     */
+    public static final int TAG_SATURATION_2 = 0x00AA;
+
+    /**
+     * The camera's digital vari-program setting, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>AUTO</code></li>
+     *     <li><code>AUTO(FLASH OFF)</code></li>
+     *     <li><code>CLOSE UP</code></li>
+     *     <li><code>LANDSCAPE</code></li>
+     *     <li><code>NIGHT PORTRAIT</code></li>
+     *     <li><code>PORTRAIT</code></li>
+     *     <li><code>SPORT</code></li>
+     * </ul>
+     */
+    public static final int TAG_DIGITAL_VARI_PROGRAM = 0x00AB;
+
+    /**
+     * The camera's digital vari-program setting, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>VR-ON</code></li>
+     *     <li><code>VR-OFF</code></li>
+     *     <li><code>VR-HYBRID</code></li>
+     *     <li><code>VR-ACTIVE</code></li>
+     * </ul>
+     */
+    public static final int TAG_IMAGE_STABILISATION = 0x00AC;
+
+    /**
+     * The camera's digital vari-program setting, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>HYBRID</code></li>
+     *     <li><code>STANDARD</code></li>
+     * </ul>
+     */
+    public static final int TAG_AF_RESPONSE = 0x00AD;
+    public static final int TAG_UNKNOWN_29 = 0x00AE;
+    public static final int TAG_UNKNOWN_30 = 0x00AF;
+    public static final int TAG_MULTI_EXPOSURE = 0x00B0;
+
+    /**
+     * The camera's high ISO noise reduction setting, as an integer.
+     * <ul>
+     *     <li><code>0</code> Off</li>
+     *     <li><code>1</code> Minimal</li>
+     *     <li><code>2</code> Low</li>
+     *     <li><code>4</code> Normal</li>
+     *     <li><code>6</code> High</li>
+     * </ul>
+     */
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION = 0x00B1;
+    public static final int TAG_UNKNOWN_31 = 0x00B2;
+    public static final int TAG_UNKNOWN_32 = 0x00B3;
+    public static final int TAG_UNKNOWN_33 = 0x00B4;
+    public static final int TAG_UNKNOWN_48 = 0x00B5;
+    public static final int TAG_POWER_UP_TIME = 0x00B6;
+    public static final int TAG_AF_INFO_2 = 0x00B7;
+    public static final int TAG_FILE_INFO = 0x00B8;
+    public static final int TAG_AF_TUNE = 0x00B9;
+    public static final int TAG_UNKNOWN_49 = 0x00BB;
+    public static final int TAG_UNKNOWN_50 = 0x00BD;
+    public static final int TAG_UNKNOWN_51 = 0x0103;
+    public static final int TAG_PRINT_IM = 0x0E00;
+
+    /**
+     * Data about changes set by Nikon Capture Editor.
+     *
+     * Values observed
+     */
+    public static final int TAG_NIKON_CAPTURE_DATA = 0x0E01;
+    public static final int TAG_UNKNOWN_52 = 0x0E05;
+    public static final int TAG_UNKNOWN_53 = 0x0E08;
+    public static final int TAG_NIKON_CAPTURE_VERSION = 0x0E09;
+    public static final int TAG_NIKON_CAPTURE_OFFSETS = 0x0E0E;
+    public static final int TAG_NIKON_SCAN = 0x0E10;
+    public static final int TAG_UNKNOWN_54 = 0x0E19;
+    public static final int TAG_NEF_BIT_DEPTH = 0x0E22;
+    public static final int TAG_UNKNOWN_55 = 0x0E23;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_ISO_1, "ISO");
+        _tagNameMap.put(TAG_QUALITY_AND_FILE_FORMAT, "Quality & File Format");
+        _tagNameMap.put(TAG_CAMERA_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_CAMERA_SHARPENING, "Sharpening");
+        _tagNameMap.put(TAG_AF_TYPE, "AF Type");
+        _tagNameMap.put(TAG_CAMERA_WHITE_BALANCE_FINE, "White Balance Fine");
+        _tagNameMap.put(TAG_CAMERA_WHITE_BALANCE_RB_COEFF, "White Balance RB Coefficients");
+        _tagNameMap.put(TAG_ISO_REQUESTED, "ISO");
+        _tagNameMap.put(TAG_ISO_MODE, "ISO Mode");
+        _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
+
+        _tagNameMap.put(TAG_PROGRAM_SHIFT, "Program Shift");
+        _tagNameMap.put(TAG_EXPOSURE_DIFFERENCE, "Exposure Difference");
+        _tagNameMap.put(TAG_PREVIEW_IFD, "Preview IFD");
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_FLASH_USED, "Flash Used");
+        _tagNameMap.put(TAG_AF_FOCUS_POSITION, "AF Focus Position");
+        _tagNameMap.put(TAG_SHOOTING_MODE, "Shooting Mode");
+        _tagNameMap.put(TAG_LENS_STOPS, "Lens Stops");
+        _tagNameMap.put(TAG_CONTRAST_CURVE, "Contrast Curve");
+        _tagNameMap.put(TAG_LIGHT_SOURCE, "Light source");
+        _tagNameMap.put(TAG_SHOT_INFO, "Shot Info");
+        _tagNameMap.put(TAG_COLOR_BALANCE, "Color Balance");
+        _tagNameMap.put(TAG_LENS_DATA, "Lens Data");
+        _tagNameMap.put(TAG_NEF_THUMBNAIL_SIZE, "NEF Thumbnail Size");
+        _tagNameMap.put(TAG_SENSOR_PIXEL_SIZE, "Sensor Pixel Size");
+        _tagNameMap.put(TAG_UNKNOWN_10, "Unknown 10");
+        _tagNameMap.put(TAG_SCENE_ASSIST, "Scene Assist");
+        _tagNameMap.put(TAG_UNKNOWN_11, "Unknown 11");
+        _tagNameMap.put(TAG_RETOUCH_HISTORY, "Retouch History");
+        _tagNameMap.put(TAG_UNKNOWN_12, "Unknown 12");
+        _tagNameMap.put(TAG_FLASH_SYNC_MODE, "Flash Sync Mode");
+        _tagNameMap.put(TAG_AUTO_FLASH_MODE, "Auto Flash Mode");
+        _tagNameMap.put(TAG_AUTO_FLASH_COMPENSATION, "Auto Flash Compensation");
+        _tagNameMap.put(TAG_EXPOSURE_SEQUENCE_NUMBER, "Exposure Sequence Number");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+
+        _tagNameMap.put(TAG_UNKNOWN_20, "Unknown 20");
+        _tagNameMap.put(TAG_IMAGE_BOUNDARY, "Image Boundary");
+        _tagNameMap.put(TAG_FLASH_EXPOSURE_COMPENSATION, "Flash Exposure Compensation");
+        _tagNameMap.put(TAG_FLASH_BRACKET_COMPENSATION, "Flash Bracket Compensation");
+        _tagNameMap.put(TAG_AE_BRACKET_COMPENSATION, "AE Bracket Compensation");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_CROP_HIGH_SPEED, "Crop High Speed");
+        _tagNameMap.put(TAG_EXPOSURE_TUNING, "Exposure Tuning");
+        _tagNameMap.put(TAG_CAMERA_SERIAL_NUMBER, "Camera Serial Number");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_VR_INFO, "VR Info");
+        _tagNameMap.put(TAG_IMAGE_AUTHENTICATION, "Image Authentication");
+        _tagNameMap.put(TAG_UNKNOWN_35, "Unknown 35");
+        _tagNameMap.put(TAG_ACTIVE_D_LIGHTING, "Active D-Lighting");
+        _tagNameMap.put(TAG_PICTURE_CONTROL, "Picture Control");
+        _tagNameMap.put(TAG_WORLD_TIME, "World Time");
+        _tagNameMap.put(TAG_ISO_INFO, "ISO Info");
+        _tagNameMap.put(TAG_UNKNOWN_36, "Unknown 36");
+        _tagNameMap.put(TAG_UNKNOWN_37, "Unknown 37");
+        _tagNameMap.put(TAG_UNKNOWN_38, "Unknown 38");
+        _tagNameMap.put(TAG_UNKNOWN_39, "Unknown 39");
+        _tagNameMap.put(TAG_VIGNETTE_CONTROL, "Vignette Control");
+        _tagNameMap.put(TAG_UNKNOWN_40, "Unknown 40");
+        _tagNameMap.put(TAG_UNKNOWN_41, "Unknown 41");
+        _tagNameMap.put(TAG_UNKNOWN_42, "Unknown 42");
+        _tagNameMap.put(TAG_UNKNOWN_43, "Unknown 43");
+        _tagNameMap.put(TAG_UNKNOWN_44, "Unknown 44");
+        _tagNameMap.put(TAG_UNKNOWN_45, "Unknown 45");
+        _tagNameMap.put(TAG_UNKNOWN_46, "Unknown 46");
+        _tagNameMap.put(TAG_UNKNOWN_47, "Unknown 47");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+
+        _tagNameMap.put(TAG_CAMERA_SERIAL_NUMBER_2, "Camera Serial Number");
+        _tagNameMap.put(TAG_IMAGE_DATA_SIZE, "Image Data Size");
+        _tagNameMap.put(TAG_UNKNOWN_27, "Unknown 27");
+        _tagNameMap.put(TAG_UNKNOWN_28, "Unknown 28");
+        _tagNameMap.put(TAG_IMAGE_COUNT, "Image Count");
+        _tagNameMap.put(TAG_DELETED_IMAGE_COUNT, "Deleted Image Count");
+        _tagNameMap.put(TAG_SATURATION_2, "Saturation");
+        _tagNameMap.put(TAG_DIGITAL_VARI_PROGRAM, "Digital Vari Program");
+        _tagNameMap.put(TAG_IMAGE_STABILISATION, "Image Stabilisation");
+        _tagNameMap.put(TAG_AF_RESPONSE, "AF Response");
+        _tagNameMap.put(TAG_UNKNOWN_29, "Unknown 29");
+        _tagNameMap.put(TAG_UNKNOWN_30, "Unknown 30");
+        _tagNameMap.put(TAG_MULTI_EXPOSURE, "Multi Exposure");
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_UNKNOWN_31, "Unknown 31");
+        _tagNameMap.put(TAG_UNKNOWN_32, "Unknown 32");
+        _tagNameMap.put(TAG_UNKNOWN_33, "Unknown 33");
+        _tagNameMap.put(TAG_UNKNOWN_48, "Unknown 48");
+        _tagNameMap.put(TAG_POWER_UP_TIME, "Power Up Time");
+        _tagNameMap.put(TAG_AF_INFO_2, "AF Info 2");
+        _tagNameMap.put(TAG_FILE_INFO, "File Info");
+        _tagNameMap.put(TAG_AF_TUNE, "AF Tune");
+        _tagNameMap.put(TAG_FLASH_INFO, "Flash Info");
+        _tagNameMap.put(TAG_IMAGE_OPTIMISATION, "Image Optimisation");
+
+        _tagNameMap.put(TAG_IMAGE_ADJUSTMENT, "Image Adjustment");
+        _tagNameMap.put(TAG_CAMERA_TONE_COMPENSATION, "Tone Compensation");
+        _tagNameMap.put(TAG_ADAPTER, "Adapter");
+        _tagNameMap.put(TAG_LENS, "Lens");
+        _tagNameMap.put(TAG_MANUAL_FOCUS_DISTANCE, "Manual Focus Distance");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_CAMERA_COLOR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_CAMERA_HUE_ADJUSTMENT, "Camera Hue Adjustment");
+        _tagNameMap.put(TAG_NEF_COMPRESSION, "NEF Compression");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_LINEARIZATION_TABLE, "Linearization Table");
+        _tagNameMap.put(TAG_NIKON_CAPTURE_DATA, "Nikon Capture Data");
+        _tagNameMap.put(TAG_UNKNOWN_49, "Unknown 49");
+        _tagNameMap.put(TAG_UNKNOWN_50, "Unknown 50");
+        _tagNameMap.put(TAG_UNKNOWN_51, "Unknown 51");
+        _tagNameMap.put(TAG_PRINT_IM, "Print IM");
+        _tagNameMap.put(TAG_UNKNOWN_52, "Unknown 52");
+        _tagNameMap.put(TAG_UNKNOWN_53, "Unknown 53");
+        _tagNameMap.put(TAG_NIKON_CAPTURE_VERSION, "Nikon Capture Version");
+        _tagNameMap.put(TAG_NIKON_CAPTURE_OFFSETS, "Nikon Capture Offsets");
+        _tagNameMap.put(TAG_NIKON_SCAN, "Nikon Scan");
+        _tagNameMap.put(TAG_UNKNOWN_54, "Unknown 54");
+        _tagNameMap.put(TAG_NEF_BIT_DEPTH, "NEF Bit Depth");
+        _tagNameMap.put(TAG_UNKNOWN_55, "Unknown 55");
+    }
+
+    public NikonType2MakernoteDirectory()
+    {
+        this.setDescriptor(new NikonType2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Nikon Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
new file mode 100644
index 0000000..f003fcb
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
@@ -0,0 +1,749 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.util.GregorianCalendar;
+
+import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link OlympusMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDirectory>
+{
+    // TODO extend support for some offset-encoded byte[] tags: http://www.ozhiker.com/electronics/pjmt/jpeg_info/olympus_mn.html
+
+    public OlympusMakernoteDescriptor(@NotNull OlympusMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case TAG_COLOUR_MODE:
+                return getColorModeDescription();
+            case TAG_IMAGE_QUALITY_1:
+                return getImageQuality1Description();
+            case TAG_IMAGE_QUALITY_2:
+                return getImageQuality2Description();
+            case TAG_SPECIAL_MODE:
+                return getSpecialModeDescription();
+            case TAG_JPEG_QUALITY:
+                return getJpegQualityDescription();
+            case TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case TAG_BW_MODE:
+                return getBWModeDescription();
+            case TAG_DIGI_ZOOM_RATIO:
+                return getDigiZoomRatioDescription();
+            case TAG_CAMERA_ID:
+                return getCameraIdDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_FOCUS_RANGE:
+                return getFocusRangeDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+
+            case CameraSettings.TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case CameraSettings.TAG_FLASH_MODE:
+                return getFlashModeCameraSettingDescription();
+            case CameraSettings.TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case CameraSettings.TAG_IMAGE_SIZE:
+                return getImageSizeDescription();
+            case CameraSettings.TAG_IMAGE_QUALITY:
+                return getImageQualityDescription();
+            case CameraSettings.TAG_SHOOTING_MODE:
+                return getShootingModeDescription();
+            case CameraSettings.TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case CameraSettings.TAG_APEX_FILM_SPEED_VALUE:
+                return getApexFilmSpeedDescription();
+            case CameraSettings.TAG_APEX_SHUTTER_SPEED_TIME_VALUE:
+                return getApexShutterSpeedTimeDescription();
+            case CameraSettings.TAG_APEX_APERTURE_VALUE:
+                return getApexApertureDescription();
+            case CameraSettings.TAG_MACRO_MODE:
+                return getMacroModeCameraSettingDescription();
+            case CameraSettings.TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case CameraSettings.TAG_EXPOSURE_COMPENSATION:
+                return getExposureCompensationDescription();
+            case CameraSettings.TAG_BRACKET_STEP:
+                return getBracketStepDescription();
+
+            case CameraSettings.TAG_INTERVAL_LENGTH:
+                return getIntervalLengthDescription();
+            case CameraSettings.TAG_INTERVAL_NUMBER:
+                return getIntervalNumberDescription();
+            case CameraSettings.TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case CameraSettings.TAG_FOCUS_DISTANCE:
+                return getFocusDistanceDescription();
+            case CameraSettings.TAG_FLASH_FIRED:
+                return getFlastFiredDescription();
+            case CameraSettings.TAG_DATE:
+                return getDateDescription();
+            case CameraSettings.TAG_TIME:
+                return getTimeDescription();
+            case CameraSettings.TAG_MAX_APERTURE_AT_FOCAL_LENGTH:
+                return getMaxApertureAtFocalLengthDescription();
+
+            case CameraSettings.TAG_FILE_NUMBER_MEMORY:
+                return getFileNumberMemoryDescription();
+            case CameraSettings.TAG_LAST_FILE_NUMBER:
+                return getLastFileNumberDescription();
+            case CameraSettings.TAG_WHITE_BALANCE_RED:
+                return getWhiteBalanceRedDescription();
+            case CameraSettings.TAG_WHITE_BALANCE_GREEN:
+                return getWhiteBalanceGreenDescription();
+            case CameraSettings.TAG_WHITE_BALANCE_BLUE:
+                return getWhiteBalanceBlueDescription();
+            case CameraSettings.TAG_SATURATION:
+                return getSaturationDescription();
+            case CameraSettings.TAG_CONTRAST:
+                return getContrastDescription();
+            case CameraSettings.TAG_SHARPNESS:
+                return getSharpnessCameraSettingDescription();
+            case CameraSettings.TAG_SUBJECT_PROGRAM:
+                return getSubjectProgramDescription();
+            case CameraSettings.TAG_FLASH_COMPENSATION:
+                return getFlastCompensationDescription();
+            case CameraSettings.TAG_ISO_SETTING:
+                return getIsoSettingDescription();
+            case CameraSettings.TAG_CAMERA_MODEL:
+                return getCameraModelDescription();
+            case CameraSettings.TAG_INTERVAL_MODE:
+                return getIntervalModeDescription();
+            case CameraSettings.TAG_FOLDER_NAME:
+                return getFolderNameDescription();
+            case CameraSettings.TAG_COLOR_MODE:
+                return getColorModeCameraSettingDescription();
+            case CameraSettings.TAG_COLOR_FILTER:
+                return getColorFilterDescription();
+            case CameraSettings.TAG_BLACK_AND_WHITE_FILTER:
+                return getBlackAndWhiteFilterDescription();
+            case CameraSettings.TAG_INTERNAL_FLASH:
+                return getInternalFlashDescription();
+            case CameraSettings.TAG_APEX_BRIGHTNESS_VALUE:
+                return getApexBrightnessDescription();
+            case CameraSettings.TAG_SPOT_FOCUS_POINT_X_COORDINATE:
+                return getSpotFocusPointXCoordinateDescription();
+            case CameraSettings.TAG_SPOT_FOCUS_POINT_Y_COORDINATE:
+                return getSpotFocusPointYCoordinateDescription();
+            case CameraSettings.TAG_WIDE_FOCUS_ZONE:
+                return getWideFocusZoneDescription();
+            case CameraSettings.TAG_FOCUS_MODE:
+                return getFocusModeCameraSettingDescription();
+            case CameraSettings.TAG_FOCUS_AREA:
+                return getFocusAreaDescription();
+            case CameraSettings.TAG_DEC_SWITCH_POSITION:
+                return getDecSwitchPositionDescription();
+
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_EXPOSURE_MODE, "P", "A", "S", "M");
+    }
+
+    @Nullable
+    public String getFlashModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FLASH_MODE,
+            "Normal", "Red-eye reduction", "Rear flash sync", "Wireless");
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_WHITE_BALANCE,
+            "Auto", // 0
+            "Daylight",
+            "Cloudy",
+            "Tungsten",
+            null,
+            "Custom", // 5
+            null,
+            "Fluorescent",
+            "Fluorescent 2",
+            null,
+            null, // 10
+            "Custom 2",
+            "Custom 3"
+        );
+    }
+
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        // This is a pretty weird way to store this information!
+        return getIndexedDescription(CameraSettings.TAG_IMAGE_SIZE, "2560 x 1920", "1600 x 1200", "1280 x 960", "640 x 480");
+    }
+
+    @Nullable
+    public String getImageQualityDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_IMAGE_QUALITY, "Raw", "Super Fine", "Fine", "Standard", "Economy", "Extra Fine");
+    }
+
+    @Nullable
+    public String getShootingModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_SHOOTING_MODE,
+            "Single",
+            "Continuous",
+            "Self Timer",
+            null,
+            "Bracketing",
+            "Interval",
+            "UHS Continuous",
+            "HS Continuous"
+        );
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_METERING_MODE, "Multi-Segment", "Centre Weighted", "Spot");
+    }
+
+    @Nullable
+    public String getApexFilmSpeedDescription()
+    {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html#Minolta_Camera_Settings
+        // Apex Speed value = value/8-1 ,
+        // ISO = (2^(value/8-1))*3.125
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_FILM_SPEED_VALUE);
+
+        if (value == null)
+            return null;
+
+        double iso = Math.pow((value / 8d) - 1, 2) * 3.125;
+        return Double.toString(iso);
+    }
+
+    @Nullable
+    public String getApexShutterSpeedTimeDescription()
+    {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html#Minolta_Camera_Settings
+        // Apex Time value = value/8-6 ,
+        // ShutterSpeed = 2^( (48-value)/8 ),
+        // Due to rounding error value=8 should be displayed as 30 sec.
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_SHUTTER_SPEED_TIME_VALUE);
+
+        if (value == null)
+            return null;
+
+        double shutterSpeed = Math.pow((49-value) / 8d, 2);
+        return Double.toString(shutterSpeed) + " sec";
+    }
+
+    @Nullable
+    public String getApexApertureDescription()
+    {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html#Minolta_Camera_Settings
+        // Apex Aperture Value = value/8-1 ,
+        // Aperture F-stop = 2^( value/16-0.5 )
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_APERTURE_VALUE);
+
+        if (value == null)
+            return null;
+
+        double fStop = Math.pow((value/16d) - 0.5, 2);
+        return "F" + Double.toString(fStop);
+    }
+
+    @Nullable
+    public String getMacroModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_MACRO_MODE, "Off", "On");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "Off", "Electronic magnification", "Digital zoom 2x");
+    }
+
+    @Nullable
+    public String getExposureCompensationDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_EXPOSURE_COMPENSATION);
+        return value == null ? null : ((value / 3d) - 2) + " EV";
+    }
+
+    @Nullable
+    public String getBracketStepDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_BRACKET_STEP, "1/3 EV", "2/3 EV", "1 EV");
+    }
+
+    @Nullable
+    public String getIntervalLengthDescription()
+    {
+        if (!_directory.isIntervalMode())
+            return "N/A";
+
+        Long value = _directory.getLongObject(CameraSettings.TAG_INTERVAL_LENGTH);
+        return value == null ? null : value + " min";
+    }
+
+    @Nullable
+    public String getIntervalNumberDescription()
+    {
+        if (!_directory.isIntervalMode())
+            return "N/A";
+
+        Long value = _directory.getLongObject(CameraSettings.TAG_INTERVAL_NUMBER);
+        return value == null ? null : Long.toString(value);
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_FOCAL_LENGTH);
+        return value == null ? null : Double.toString(value/256d) + " mm";
+    }
+
+    @Nullable
+    public String getFocusDistanceDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_FOCUS_DISTANCE);
+        return value == null
+            ? null
+            : value == 0
+                ? "Infinity"
+                : value + " mm";
+    }
+
+    @Nullable
+    public String getFlastFiredDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FLASH_FIRED, "No", "Yes");
+    }
+
+    @Nullable
+    public String getDateDescription()
+    {
+        // day = value%256,
+        // month = floor( (value - floor( value/65536 )*65536 )/256 )
+        // year = floor( value/65536)
+        Long value = _directory.getLongObject(CameraSettings.TAG_DATE);
+        if (value == null)
+            return null;
+        long day = value & 0xFF;
+        long month = (value >> 16) & 0xFF;
+        long year = (value >> 8) & 0xFF;
+        return new GregorianCalendar((int)year + 1970, (int)month, (int)day).getTime().toString();
+    }
+
+    @Nullable
+    public String getTimeDescription()
+    {
+        // hours = floor( value/65536 ),
+        // minutes = floor( ( value - floor( value/65536 )*65536 )/256 ),
+        // seconds = value%256
+        Long value = _directory.getLongObject(CameraSettings.TAG_TIME);
+        if (value == null)
+            return null;
+        long hours = (value >> 8) & 0xFF;
+        long minutes = (value >> 16) & 0xFF;
+        long seconds = value & 0xFF;
+
+        return String.format("%02d:%02d:%02d", hours, minutes, seconds);
+    }
+
+    @Nullable
+    public String getMaxApertureAtFocalLengthDescription()
+    {
+        // Aperture F-Stop = 2^(value/16-0.5)
+        Long value = _directory.getLongObject(CameraSettings.TAG_TIME);
+        if (value == null)
+            return null;
+        double fStop = Math.pow((value/16d) - 0.5, 2);
+        return "F" + fStop;
+    }
+
+    @Nullable
+    public String getFileNumberMemoryDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FILE_NUMBER_MEMORY, "Off", "On");
+    }
+
+    @Nullable
+    public String getLastFileNumberDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_LAST_FILE_NUMBER);
+        return value == null
+            ? null
+            : value == 0
+                ? "File Number Memory Off"
+                : Long.toString(value);
+    }
+
+    @Nullable
+    public String getWhiteBalanceRedDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_RED);
+        return value == null ? null : Double.toString(value/256d);
+    }
+
+    @Nullable
+    public String getWhiteBalanceGreenDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_GREEN);
+        return value == null ? null : Double.toString(value/256d);
+    }
+
+    @Nullable
+    public String getWhiteBalanceBlueDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_BLUE);
+        return value == null ? null : Double.toString(value/256d);
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_SATURATION);
+        return value == null ? null : Long.toString(value-3);
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_CONTRAST);
+        return value == null ? null : Long.toString(value-3);
+    }
+
+    @Nullable
+    public String getSharpnessCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_SHARPNESS, "Hard", "Normal", "Soft");
+    }
+
+    @Nullable
+    public String getSubjectProgramDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_SUBJECT_PROGRAM, "None", "Portrait", "Text", "Night Portrait", "Sunset", "Sports Action");
+    }
+
+    @Nullable
+    public String getFlastCompensationDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_FLASH_COMPENSATION);
+        return value == null ? null : ((value-6)/3d) + " EV";
+    }
+
+    @Nullable
+    public String getIsoSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_ISO_SETTING, "100", "200", "400", "800", "Auto", "64");
+    }
+
+    @Nullable
+    public String getCameraModelDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_CAMERA_MODEL,
+            "DiMAGE 7",
+            "DiMAGE 5",
+            "DiMAGE S304",
+            "DiMAGE S404",
+            "DiMAGE 7i",
+            "DiMAGE 7Hi",
+            "DiMAGE A1",
+            "DiMAGE S414");
+    }
+
+    @Nullable
+    public String getIntervalModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_INTERVAL_MODE, "Still Image", "Time Lapse Movie");
+    }
+
+    @Nullable
+    public String getFolderNameDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOLDER_NAME, "Standard Form", "Data Form");
+    }
+
+    @Nullable
+    public String getColorModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_COLOR_MODE, "Natural Color", "Black & White", "Vivid Color", "Solarization", "AdobeRGB");
+    }
+
+    @Nullable
+    public String getColorFilterDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_COLOR_FILTER);
+        return value == null ? null : Long.toString(value-3);
+    }
+
+    @Nullable
+    public String getBlackAndWhiteFilterDescription()
+    {
+        return super.getDescription(CameraSettings.TAG_BLACK_AND_WHITE_FILTER);
+    }
+
+    @Nullable
+    public String getInternalFlashDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_INTERNAL_FLASH, "Did Not Fire", "Fired");
+    }
+
+    @Nullable
+    public String getApexBrightnessDescription()
+    {
+        Long value = _directory.getLongObject(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE);
+        return value == null ? null : Double.toString((value/8d)-6);
+    }
+
+    @Nullable
+    public String getSpotFocusPointXCoordinateDescription()
+    {
+        return super.getDescription(CameraSettings.TAG_SPOT_FOCUS_POINT_X_COORDINATE);
+    }
+
+    @Nullable
+    public String getSpotFocusPointYCoordinateDescription()
+    {
+        return super.getDescription(CameraSettings.TAG_SPOT_FOCUS_POINT_Y_COORDINATE);
+    }
+
+    @Nullable
+    public String getWideFocusZoneDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_WIDE_FOCUS_ZONE,
+            "No Zone or AF Failed",
+            "Center Zone (Horizontal Orientation)",
+            "Center Zone (Vertical Orientation)",
+            "Left Zone",
+            "Right Zone"
+        );
+    }
+
+    @Nullable
+    public String getFocusModeCameraSettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_MODE, "Auto Focus", "Manual Focus");
+    }
+
+    @Nullable
+    public String getFocusAreaDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_AREA, "Wide Focus (Normal)", "Spot Focus");
+    }
+
+    @Nullable
+    public String getDecSwitchPositionDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_DEC_SWITCH_POSITION, "Exposure", "Contrast", "Saturation", "Filter");
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getImageQuality2Description()
+    {
+        return getIndexedDescription(TAG_IMAGE_QUALITY_2,
+            "Raw",
+            "Super Fine",
+            "Fine",
+            "Standard",
+            "Extra Fine");
+    }
+
+    @Nullable
+    public String getImageQuality1Description()
+    {
+        return getIndexedDescription(TAG_IMAGE_QUALITY_1,
+            "Raw",
+            "Super Fine",
+            "Fine",
+            "Standard",
+            "Extra Fine");
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOUR_MODE,
+            "Natural Colour",
+            "Black & White",
+            "Vivid Colour",
+            "Solarization",
+            "AdobeRGB");
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal", "Hard", "Soft");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, "Auto", "Manual");
+    }
+
+    @Nullable
+    public String getFocusRangeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_RANGE, "Normal", "Macro");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE, null, null, "On", "Off");
+    }
+
+    @Nullable
+    public String getDigiZoomRatioDescription()
+    {
+        return getIndexedDescription(TAG_DIGI_ZOOM_RATIO, "Normal", null, "Digital 2x Zoom");
+    }
+
+    @Nullable
+    public String getCameraIdDescription()
+    {
+        byte[] bytes = _directory.getByteArray(TAG_CAMERA_ID);
+        if (bytes == null)
+            return null;
+        return new String(bytes);
+    }
+
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(TAG_MACRO_MODE, "Normal (no macro)", "Macro");
+    }
+
+    @Nullable
+    public String getBWModeDescription()
+    {
+        return getIndexedDescription(TAG_BW_MODE, "Off", "On");
+    }
+
+    @Nullable
+    public String getJpegQualityDescription()
+    {
+        return getIndexedDescription(TAG_JPEG_QUALITY,
+            1,
+            "Standard Quality",
+            "High Quality",
+            "Super High Quality");
+    }
+
+    @Nullable
+    public String getSpecialModeDescription()
+    {
+        long[] values = (long[])_directory.getObject(TAG_SPECIAL_MODE);
+        if (values==null)
+            return null;
+        if (values.length < 1)
+            return "";
+        StringBuilder desc = new StringBuilder();
+
+        switch ((int)values[0]) {
+            case 0:
+                desc.append("Normal picture taking mode");
+                break;
+            case 1:
+                desc.append("Unknown picture taking mode");
+                break;
+            case 2:
+                desc.append("Fast picture taking mode");
+                break;
+            case 3:
+                desc.append("Panorama picture taking mode");
+                break;
+            default:
+                desc.append("Unknown picture taking mode");
+                break;
+        }
+
+        if (values.length >= 2) {
+            switch ((int)values[1]) {
+                case 0:
+                    break;
+                case 1:
+                    desc.append(" / 1st in a sequence");
+                    break;
+                case 2:
+                    desc.append(" / 2nd in a sequence");
+                    break;
+                case 3:
+                    desc.append(" / 3rd in a sequence");
+                    break;
+                default:
+                    desc.append(" / ");
+                    desc.append(values[1]);
+                    desc.append("th in a sequence");
+                    break;
+            }
+        }
+        if (values.length >= 3) {
+            switch ((int)values[2]) {
+                case 1:
+                    desc.append(" / Left to right panorama direction");
+                    break;
+                case 2:
+                    desc.append(" / Right to left panorama direction");
+                    break;
+                case 3:
+                    desc.append(" / Bottom to top panorama direction");
+                    break;
+                case 4:
+                    desc.append(" / Top to bottom panorama direction");
+                    break;
+            }
+        }
+
+        return desc.toString();
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
new file mode 100644
index 0000000..f1e7257
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * The Olympus makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class OlympusMakernoteDirectory extends Directory
+{
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_MAKERNOTE_VERSION = 0x0000;
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_CAMERA_SETTINGS_1 = 0x0001;
+    /** Alternate Camera Settings Tag. Used by Konica / Minolta cameras. */
+    public static final int TAG_CAMERA_SETTINGS_2 = 0x0003;
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_COMPRESSED_IMAGE_SIZE = 0x0040;
+    /** Used by Konica / Minolta cameras. */
+    public static final int TAG_MINOLTA_THUMBNAIL_OFFSET_1 = 0x0081;
+    /** Alternate Thumbnail Offset. Used by Konica / Minolta cameras. */
+    public static final int TAG_MINOLTA_THUMBNAIL_OFFSET_2 = 0x0088;
+    /** Length of thumbnail in bytes. Used by Konica / Minolta cameras. */
+    public static final int TAG_MINOLTA_THUMBNAIL_LENGTH = 0x0089;
+
+    public static final int TAG_THUMBNAIL_IMAGE = 0x0100;
+
+    /**
+     * Used by Konica / Minolta cameras
+     * 0 = Natural Colour
+     * 1 = Black & White
+     * 2 = Vivid colour
+     * 3 = Solarization
+     * 4 = AdobeRGB
+     */
+    public static final int TAG_COLOUR_MODE = 0x0101;
+
+    /**
+     * Used by Konica / Minolta cameras.
+     * 0 = Raw
+     * 1 = Super Fine
+     * 2 = Fine
+     * 3 = Standard
+     * 4 = Extra Fine
+     */
+    public static final int TAG_IMAGE_QUALITY_1 = 0x0102;
+
+    /**
+     * Not 100% sure about this tag.
+     * <p>
+     * Used by Konica / Minolta cameras.
+     * 0 = Raw
+     * 1 = Super Fine
+     * 2 = Fine
+     * 3 = Standard
+     * 4 = Extra Fine
+     */
+    public static final int TAG_IMAGE_QUALITY_2 = 0x0103;
+
+    public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104;
+
+    /**
+     * Three values:
+     * Value 1: 0=Normal, 2=Fast, 3=Panorama
+     * Value 2: Sequence Number Value 3:
+     * 1 = Panorama Direction: Left to Right
+     * 2 = Panorama Direction: Right to Left
+     * 3 = Panorama Direction: Bottom to Top
+     * 4 = Panorama Direction: Top to Bottom
+     */
+    public static final int TAG_SPECIAL_MODE = 0x0200;
+
+    /**
+     * 1 = Standard Quality
+     * 2 = High Quality
+     * 3 = Super High Quality
+     */
+    public static final int TAG_JPEG_QUALITY = 0x0201;
+
+    /**
+     * 0 = Normal (Not Macro)
+     * 1 = Macro
+     */
+    public static final int TAG_MACRO_MODE = 0x0202;
+
+    /**
+     * 0 = Off, 1 = On
+     */
+    public static final int TAG_BW_MODE = 0x0203;
+
+    /** Zoom Factor (0 or 1 = normal) */
+    public static final int TAG_DIGI_ZOOM_RATIO = 0x0204;
+    public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0205;
+    public static final int TAG_LENS_DISTORTION_PARAMETERS = 0x0206;
+    public static final int TAG_FIRMWARE_VERSION = 0x0207;
+    public static final int TAG_PICT_INFO = 0x0208;
+    public static final int TAG_CAMERA_ID = 0x0209;
+
+    /**
+     * Used by Epson cameras
+     * Units = pixels
+     */
+    public static final int TAG_IMAGE_WIDTH = 0x020B;
+
+    /**
+     * Used by Epson cameras
+     * Units = pixels
+     */
+    public static final int TAG_IMAGE_HEIGHT = 0x020C;
+
+    /** A string. Used by Epson cameras. */
+    public static final int TAG_ORIGINAL_MANUFACTURER_MODEL = 0x020D;
+
+    public static final int TAG_PREVIEW_IMAGE = 0x0280;
+    public static final int TAG_PRE_CAPTURE_FRAMES = 0x0300;
+    public static final int TAG_WHITE_BOARD = 0x0301;
+    public static final int TAG_ONE_TOUCH_WB = 0x0302;
+    public static final int TAG_WHITE_BALANCE_BRACKET = 0x0303;
+    public static final int TAG_WHITE_BALANCE_BIAS = 0x0304;
+    public static final int TAG_SCENE_MODE = 0x0403;
+    public static final int TAG_FIRMWARE = 0x0404;
+
+    /**
+     * See the PIM specification here:
+     * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    public static final int TAG_DATA_DUMP_1 = 0x0F00;
+    public static final int TAG_DATA_DUMP_2 = 0x0F01;
+
+    public static final int TAG_SHUTTER_SPEED_VALUE = 0x1000;
+    public static final int TAG_ISO_VALUE = 0x1001;
+    public static final int TAG_APERTURE_VALUE = 0x1002;
+    public static final int TAG_BRIGHTNESS_VALUE = 0x1003;
+    public static final int TAG_FLASH_MODE = 0x1004;
+    public static final int TAG_FLASH_DEVICE = 0x1005;
+    public static final int TAG_BRACKET = 0x1006;
+    public static final int TAG_SENSOR_TEMPERATURE = 0x1007;
+    public static final int TAG_LENS_TEMPERATURE = 0x1008;
+    public static final int TAG_LIGHT_CONDITION = 0x1009;
+    public static final int TAG_FOCUS_RANGE = 0x100A;
+    public static final int TAG_FOCUS_MODE = 0x100B;
+    public static final int TAG_FOCUS_DISTANCE = 0x100C;
+    public static final int TAG_ZOOM = 0x100D;
+    public static final int TAG_MACRO_FOCUS = 0x100E;
+    public static final int TAG_SHARPNESS = 0x100F;
+    public static final int TAG_FLASH_CHARGE_LEVEL = 0x1010;
+    public static final int TAG_COLOUR_MATRIX = 0x1011;
+    public static final int TAG_BLACK_LEVEL = 0x1012;
+//    public static final int TAG_ = 0x1013;
+//    public static final int TAG_ = 0x1014;
+    public static final int TAG_WHITE_BALANCE = 0x1015;
+//    public static final int TAG_ = 0x1016;
+    public static final int TAG_RED_BIAS = 0x1017;
+    public static final int TAG_BLUE_BIAS = 0x1018;
+    public static final int TAG_COLOR_MATRIX_NUMBER = 0x1019;
+    public static final int TAG_SERIAL_NUMBER = 0x101A;
+//    public static final int TAG_ = 0x101B;
+//    public static final int TAG_ = 0x101C;
+//    public static final int TAG_ = 0x101D;
+//    public static final int TAG_ = 0x101E;
+//    public static final int TAG_ = 0x101F;
+//    public static final int TAG_ = 0x1020;
+//    public static final int TAG_ = 0x1021;
+//    public static final int TAG_ = 0x1022;
+    public static final int TAG_FLASH_BIAS = 0x1023;
+//    public static final int TAG_ = 0x1024;
+//    public static final int TAG_ = 0x1025;
+    public static final int TAG_EXTERNAL_FLASH_BOUNCE = 0x1026;
+    public static final int TAG_EXTERNAL_FLASH_ZOOM = 0x1027;
+    public static final int TAG_EXTERNAL_FLASH_MODE = 0x1028;
+    public static final int TAG_CONTRAST = 0x1029;
+    public static final int TAG_SHARPNESS_FACTOR = 0x102A;
+    public static final int TAG_COLOUR_CONTROL = 0x102B;
+    public static final int TAG_VALID_BITS = 0x102C;
+    public static final int TAG_CORING_FILTER = 0x102D;
+    public static final int TAG_FINAL_WIDTH = 0x102E;
+    public static final int TAG_FINAL_HEIGHT = 0x102F;
+//    public static final int TAG_ = 0x1030;
+//    public static final int TAG_ = 0x1031;
+//    public static final int TAG_ = 0x1032;
+//    public static final int TAG_ = 0x1033;
+    public static final int TAG_COMPRESSION_RATIO = 0x1034;
+    public static final int TAG_THUMBNAIL = 0x1035;
+    public static final int TAG_THUMBNAIL_OFFSET = 0x1036;
+    public static final int TAG_THUMBNAIL_LENGTH = 0x1037;
+//    public static final int TAG_ = 0x1038;
+    public static final int TAG_CCD_SCAN_MODE = 0x1039;
+    public static final int TAG_NOISE_REDUCTION = 0x103A;
+    public static final int TAG_INFINITY_LENS_STEP = 0x103B;
+    public static final int TAG_NEAR_LENS_STEP = 0x103C;
+    public static final int TAG_EQUIPMENT = 0x2010;
+    public static final int TAG_CAMERA_SETTINGS = 0x2020;
+    public static final int TAG_RAW_DEVELOPMENT = 0x2030;
+    public static final int TAG_RAW_DEVELOPMENT_2 = 0x2031;
+    public static final int TAG_IMAGE_PROCESSING = 0x2040;
+    public static final int TAG_FOCUS_INFO = 0x2050;
+    public static final int TAG_RAW_INFO = 0x3000;
+
+    public final static class CameraSettings
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the Makernote IFD
+        private static final int OFFSET = 0xF000;
+
+        public static final int TAG_EXPOSURE_MODE = OFFSET + 2;
+        public static final int TAG_FLASH_MODE = OFFSET + 3;
+        public static final int TAG_WHITE_BALANCE = OFFSET + 4;
+        public static final int TAG_IMAGE_SIZE = OFFSET + 5;
+        public static final int TAG_IMAGE_QUALITY = OFFSET + 6;
+        public static final int TAG_SHOOTING_MODE = OFFSET + 7;
+        public static final int TAG_METERING_MODE = OFFSET + 8;
+        public static final int TAG_APEX_FILM_SPEED_VALUE = OFFSET + 9;
+        public static final int TAG_APEX_SHUTTER_SPEED_TIME_VALUE = OFFSET + 10;
+        public static final int TAG_APEX_APERTURE_VALUE = OFFSET + 11;
+        public static final int TAG_MACRO_MODE = OFFSET + 12;
+        public static final int TAG_DIGITAL_ZOOM = OFFSET + 13;
+        public static final int TAG_EXPOSURE_COMPENSATION = OFFSET + 14;
+        public static final int TAG_BRACKET_STEP = OFFSET + 15;
+        // 16 missing
+        public static final int TAG_INTERVAL_LENGTH = OFFSET + 17;
+        public static final int TAG_INTERVAL_NUMBER = OFFSET + 18;
+        public static final int TAG_FOCAL_LENGTH = OFFSET + 19;
+        public static final int TAG_FOCUS_DISTANCE = OFFSET + 20;
+        public static final int TAG_FLASH_FIRED = OFFSET + 21;
+        public static final int TAG_DATE = OFFSET + 22;
+        public static final int TAG_TIME = OFFSET + 23;
+        public static final int TAG_MAX_APERTURE_AT_FOCAL_LENGTH = OFFSET + 24;
+        // 25, 26 missing
+        public static final int TAG_FILE_NUMBER_MEMORY = OFFSET + 27;
+        public static final int TAG_LAST_FILE_NUMBER = OFFSET + 28;
+        public static final int TAG_WHITE_BALANCE_RED = OFFSET + 29;
+        public static final int TAG_WHITE_BALANCE_GREEN = OFFSET + 30;
+        public static final int TAG_WHITE_BALANCE_BLUE = OFFSET + 31;
+        public static final int TAG_SATURATION = OFFSET + 32;
+        public static final int TAG_CONTRAST = OFFSET + 33;
+        public static final int TAG_SHARPNESS = OFFSET + 34;
+        public static final int TAG_SUBJECT_PROGRAM = OFFSET + 35;
+        public static final int TAG_FLASH_COMPENSATION = OFFSET + 36;
+        public static final int TAG_ISO_SETTING = OFFSET + 37;
+        public static final int TAG_CAMERA_MODEL = OFFSET + 38;
+        public static final int TAG_INTERVAL_MODE = OFFSET + 39;
+        public static final int TAG_FOLDER_NAME = OFFSET + 40;
+        public static final int TAG_COLOR_MODE = OFFSET + 41;
+        public static final int TAG_COLOR_FILTER = OFFSET + 42;
+        public static final int TAG_BLACK_AND_WHITE_FILTER = OFFSET + 43;
+        public static final int TAG_INTERNAL_FLASH = OFFSET + 44;
+        public static final int TAG_APEX_BRIGHTNESS_VALUE = OFFSET + 45;
+        public static final int TAG_SPOT_FOCUS_POINT_X_COORDINATE = OFFSET + 46;
+        public static final int TAG_SPOT_FOCUS_POINT_Y_COORDINATE = OFFSET + 47;
+        public static final int TAG_WIDE_FOCUS_ZONE = OFFSET + 48;
+        public static final int TAG_FOCUS_MODE = OFFSET + 49;
+        public static final int TAG_FOCUS_AREA = OFFSET + 50;
+        public static final int TAG_DEC_SWITCH_POSITION = OFFSET + 51;
+    }
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS_1, "Camera Settings");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS_2, "Camera Settings");
+        _tagNameMap.put(TAG_COMPRESSED_IMAGE_SIZE, "Compressed Image Size");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_1, "Thumbnail Offset");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_2, "Thumbnail Offset");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_THUMBNAIL_IMAGE, "Thumbnail Image");
+        _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_IMAGE_QUALITY_1, "Image Quality");
+        _tagNameMap.put(TAG_IMAGE_QUALITY_2, "Image Quality");
+        _tagNameMap.put(TAG_BODY_FIRMWARE_VERSION, "Body Firmware Version");
+        _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode");
+        _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality");
+        _tagNameMap.put(TAG_MACRO_MODE, "Macro");
+        _tagNameMap.put(TAG_BW_MODE, "BW Mode");
+        _tagNameMap.put(TAG_DIGI_ZOOM_RATIO, "DigiZoom Ratio");
+        _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
+        _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters");
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_PICT_INFO, "Pict Info");
+        _tagNameMap.put(TAG_CAMERA_ID, "Camera Id");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_ORIGINAL_MANUFACTURER_MODEL, "Original Manufacturer Model");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE, "Preview Image");
+        _tagNameMap.put(TAG_PRE_CAPTURE_FRAMES, "Pre Capture Frames");
+        _tagNameMap.put(TAG_WHITE_BOARD, "White Board");
+        _tagNameMap.put(TAG_ONE_TOUCH_WB, "One Touch WB");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BRACKET, "White Balance Bracket");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_FIRMWARE, "Firmware");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_DATA_DUMP_1, "Data Dump");
+        _tagNameMap.put(TAG_DATA_DUMP_2, "Data Dump 2");
+        _tagNameMap.put(TAG_SHUTTER_SPEED_VALUE, "Shutter Speed Value");
+        _tagNameMap.put(TAG_ISO_VALUE, "ISO Value");
+        _tagNameMap.put(TAG_APERTURE_VALUE, "Aperture Value");
+        _tagNameMap.put(TAG_BRIGHTNESS_VALUE, "Brightness Value");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FLASH_DEVICE, "Flash Device");
+        _tagNameMap.put(TAG_BRACKET, "Bracket");
+        _tagNameMap.put(TAG_SENSOR_TEMPERATURE, "Sensor Temperature");
+        _tagNameMap.put(TAG_LENS_TEMPERATURE, "Lens Temperature");
+        _tagNameMap.put(TAG_LIGHT_CONDITION, "Light Condition");
+        _tagNameMap.put(TAG_FOCUS_RANGE, "Focus Range");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FOCUS_DISTANCE, "Focus Distance");
+        _tagNameMap.put(TAG_ZOOM, "Zoom");
+        _tagNameMap.put(TAG_MACRO_FOCUS, "Macro Focus");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_FLASH_CHARGE_LEVEL, "Flash Charge Level");
+        _tagNameMap.put(TAG_COLOUR_MATRIX, "Colour Matrix");
+        _tagNameMap.put(TAG_BLACK_LEVEL, "Black Level");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_RED_BIAS, "Red Bias");
+        _tagNameMap.put(TAG_BLUE_BIAS, "Blue Bias");
+        _tagNameMap.put(TAG_COLOR_MATRIX_NUMBER, "Color Matrix Number");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_BOUNCE, "External Flash Bounce");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_ZOOM, "External Flash Zoom");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_MODE, "External Flash Mode");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SHARPNESS_FACTOR, "Sharpness Factor");
+        _tagNameMap.put(TAG_COLOUR_CONTROL, "Colour Control");
+        _tagNameMap.put(TAG_VALID_BITS, "Valid Bits");
+        _tagNameMap.put(TAG_CORING_FILTER, "Coring Filter");
+        _tagNameMap.put(TAG_FINAL_WIDTH, "Final Width");
+        _tagNameMap.put(TAG_FINAL_HEIGHT, "Final Height");
+        _tagNameMap.put(TAG_COMPRESSION_RATIO, "Compression Ratio");
+        _tagNameMap.put(TAG_THUMBNAIL, "Thumbnail");
+        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_CCD_SCAN_MODE, "CCD Scan Mode");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_INFINITY_LENS_STEP, "Infinity Lens Step");
+        _tagNameMap.put(TAG_NEAR_LENS_STEP, "Near Lens Step");
+        _tagNameMap.put(TAG_EQUIPMENT, "Equipment");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings");
+        _tagNameMap.put(TAG_RAW_DEVELOPMENT, "Raw Development");
+        _tagNameMap.put(TAG_RAW_DEVELOPMENT_2, "Raw Development 2");
+        _tagNameMap.put(TAG_IMAGE_PROCESSING, "Image Processing");
+        _tagNameMap.put(TAG_FOCUS_INFO, "Focus Info");
+        _tagNameMap.put(TAG_RAW_INFO, "Raw Info");
+
+        _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(CameraSettings.TAG_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(CameraSettings.TAG_IMAGE_QUALITY, "Image Quality");
+        _tagNameMap.put(CameraSettings.TAG_SHOOTING_MODE, "Shooting Mode");
+        _tagNameMap.put(CameraSettings.TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(CameraSettings.TAG_APEX_FILM_SPEED_VALUE, "Apex Film Speed Value");
+        _tagNameMap.put(CameraSettings.TAG_APEX_SHUTTER_SPEED_TIME_VALUE, "Apex Shutter Speed Time Value");
+        _tagNameMap.put(CameraSettings.TAG_APEX_APERTURE_VALUE, "Apex Aperture Value");
+        _tagNameMap.put(CameraSettings.TAG_MACRO_MODE, "Macro Mode");
+        _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(CameraSettings.TAG_EXPOSURE_COMPENSATION, "Exposure Compensation");
+        _tagNameMap.put(CameraSettings.TAG_BRACKET_STEP, "Bracket Step");
+
+        _tagNameMap.put(CameraSettings.TAG_INTERVAL_LENGTH, "Interval Length");
+        _tagNameMap.put(CameraSettings.TAG_INTERVAL_NUMBER, "Interval Number");
+        _tagNameMap.put(CameraSettings.TAG_FOCAL_LENGTH, "Focal Length");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_DISTANCE, "Focus Distance");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_FIRED, "Flash Fired");
+        _tagNameMap.put(CameraSettings.TAG_DATE, "Date");
+        _tagNameMap.put(CameraSettings.TAG_TIME, "Time");
+        _tagNameMap.put(CameraSettings.TAG_MAX_APERTURE_AT_FOCAL_LENGTH, "Max Aperture at Focal Length");
+
+        _tagNameMap.put(CameraSettings.TAG_FILE_NUMBER_MEMORY, "File Number Memory");
+        _tagNameMap.put(CameraSettings.TAG_LAST_FILE_NUMBER, "Last File Number");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE_RED, "White Balance Red");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE_GREEN, "White Balance Green");
+        _tagNameMap.put(CameraSettings.TAG_WHITE_BALANCE_BLUE, "White Balance Blue");
+        _tagNameMap.put(CameraSettings.TAG_SATURATION, "Saturation");
+        _tagNameMap.put(CameraSettings.TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(CameraSettings.TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(CameraSettings.TAG_SUBJECT_PROGRAM, "Subject Program");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_COMPENSATION, "Flash Compensation");
+        _tagNameMap.put(CameraSettings.TAG_ISO_SETTING, "ISO Setting");
+        _tagNameMap.put(CameraSettings.TAG_CAMERA_MODEL, "Camera Model");
+        _tagNameMap.put(CameraSettings.TAG_INTERVAL_MODE, "Interval Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOLDER_NAME, "Folder Name");
+        _tagNameMap.put(CameraSettings.TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(CameraSettings.TAG_COLOR_FILTER, "Color Filter");
+        _tagNameMap.put(CameraSettings.TAG_BLACK_AND_WHITE_FILTER, "Black and White Filter");
+        _tagNameMap.put(CameraSettings.TAG_INTERNAL_FLASH, "Internal Flash");
+        _tagNameMap.put(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE, "Apex Brightness Value");
+        _tagNameMap.put(CameraSettings.TAG_SPOT_FOCUS_POINT_X_COORDINATE, "Spot Focus Point X Coordinate");
+        _tagNameMap.put(CameraSettings.TAG_SPOT_FOCUS_POINT_Y_COORDINATE, "Spot Focus Point Y Coordinate");
+        _tagNameMap.put(CameraSettings.TAG_WIDE_FOCUS_ZONE, "Wide Focus Zone");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_AREA, "Focus Area");
+        _tagNameMap.put(CameraSettings.TAG_DEC_SWITCH_POSITION, "DEC Switch Position");
+    }
+
+    public OlympusMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Makernote";
+    }
+
+    @Override
+    public void setByteArray(int tagType, @NotNull byte[] bytes)
+    {
+        if (tagType == TAG_CAMERA_SETTINGS_1 || tagType == TAG_CAMERA_SETTINGS_2) {
+            processCameraSettings(bytes);
+        } else {
+            super.setByteArray(tagType, bytes);
+        }
+    }
+
+    private void processCameraSettings(byte[] bytes)
+    {
+        SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes);
+        reader.setMotorolaByteOrder(true);
+
+        int count = bytes.length / 4;
+
+        try {
+            for (int i = 0; i < count; i++) {
+                int value = reader.getInt32();
+                setInt(CameraSettings.OFFSET + i, value);
+            }
+        } catch (IOException e) {
+            // Should never happen, given that we check the length of the bytes beforehand.
+            e.printStackTrace();
+        }
+    }
+
+    public boolean isIntervalMode()
+    {
+        Long value = getLongObject(CameraSettings.TAG_SHOOTING_MODE);
+        return value != null && value == 5;
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
new file mode 100644
index 0000000..71ca518
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Age;
+import com.drew.metadata.Face;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.IOException;
+
+import static com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicMakernoteDirectory}.
+ * <p>
+ * Some information about this makernote taken from here:
+ * <ul>
+ * <li><a href="http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html">http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html</a></li>
+ * <li><a href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html">http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html</a></li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Philipp Sandhaus
+ */
+public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory>
+{
+    public PanasonicMakernoteDescriptor(@NotNull PanasonicMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_QUALITY_MODE:
+                return getQualityModeDescription();
+            case TAG_FIRMWARE_VERSION:
+                return getVersionDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_AF_AREA_MODE:
+                return getAfAreaModeDescription();
+            case TAG_IMAGE_STABILIZATION:
+                return getImageStabilizationDescription();
+            case TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case TAG_RECORD_MODE:
+                return getRecordModeDescription();
+            case TAG_AUDIO:
+                return getAudioDescription();
+            case TAG_UNKNOWN_DATA_DUMP:
+                return getUnknownDataDumpDescription();
+            case TAG_COLOR_EFFECT:
+                return getColorEffectDescription();
+            case TAG_UPTIME:
+                return getUptimeDescription();
+            case TAG_BURST_MODE:
+                return getBurstModeDescription();
+            case TAG_CONTRAST_MODE:
+                return getContrastModeDescription();
+            case TAG_NOISE_REDUCTION:
+                return getNoiseReductionDescription();
+            case TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case TAG_ROTATION:
+                return getRotationDescription();
+            case TAG_AF_ASSIST_LAMP:
+                return getAfAssistLampDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_OPTICAL_ZOOM_MODE:
+                return getOpticalZoomModeDescription();
+            case TAG_CONVERSION_LENS:
+                return getConversionLensDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_WORLD_TIME_LOCATION:
+                return getWorldTimeLocationDescription();
+            case TAG_ADVANCED_SCENE_MODE:
+                return getAdvancedSceneModeDescription();
+            case TAG_FACE_DETECTION_INFO:
+                return getDetectedFacesDescription();
+            case TAG_TRANSFORM:
+                return getTransformDescription();
+			case TAG_TRANSFORM_1:
+	            return getTransform1Description();
+            case TAG_INTELLIGENT_EXPOSURE:
+                return getIntelligentExposureDescription();
+            case TAG_FLASH_WARNING:
+                return getFlashWarningDescription();
+            case TAG_COUNTRY:
+                return getCountryDescription();
+            case TAG_STATE:
+                return getStateDescription();
+            case TAG_CITY:
+                return getCityDescription();
+            case TAG_LANDMARK:
+                return getLandmarkDescription();
+            case TAG_INTELLIGENT_RESOLUTION:
+                return getIntelligentResolutionDescription();
+            case TAG_FACE_RECOGNITION_INFO:
+                return getRecognizedFacesDescription();
+            case TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case TAG_SCENE_MODE:
+                return getSceneModeDescription();
+            case TAG_FLASH_FIRED:
+                return getFlashFiredDescription();
+            case TAG_TEXT_STAMP:
+		        return getTextStampDescription();
+			case TAG_TEXT_STAMP_1:
+	             return getTextStamp1Description();
+			case TAG_TEXT_STAMP_2:
+		         return getTextStamp2Description();
+			case TAG_TEXT_STAMP_3:
+			     return getTextStamp3Description();
+            case TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case TAG_INTERNAL_SERIAL_NUMBER:
+                return getInternalSerialNumberDescription();
+            case TAG_TITLE:
+	            return getTitleDescription();
+			case TAG_BABY_NAME:
+	            return getBabyNameDescription();
+			case TAG_LOCATION:
+	            return getLocationDescription();
+			case TAG_BABY_AGE:
+		        return getBabyAgeDescription();
+			case TAG_BABY_AGE_1:
+		        return getBabyAge1Description();
+			default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
+    {
+        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
+    }
+
+    @Nullable
+    public String getTextStampDescription()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getTextStamp1Description()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP_1, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getTextStamp2Description()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP_2, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getTextStamp3Description()
+    {
+        return getIndexedDescription(TAG_TEXT_STAMP_3, 1, "Off", "On");
+    }
+
+	@Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(TAG_MACRO_MODE, 1, "Off", "On");
+    }
+
+    @Nullable
+    public String getFlashFiredDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_FIRED, 1, "Off", "On");
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_STABILIZATION,
+            2,
+            "On, Mode 1",
+            "Off",
+            "On, Mode 2"
+        );
+    }
+
+    @Nullable
+    public String getAudioDescription()
+    {
+        return getIndexedDescription(TAG_AUDIO, 1, "Off", "On");
+    }
+
+    @Nullable
+    public String getTransformDescription()
+    {
+        return getTransformDescription(TAG_TRANSFORM);
+    }
+
+    @Nullable
+    public String getTransform1Description()
+    {
+        return getTransformDescription(TAG_TRANSFORM_1);
+    }
+
+    @Nullable
+    private String getTransformDescription(int tag)
+    {
+        byte[] values = _directory.getByteArray(tag);
+        if (values == null)
+            return null;
+
+        RandomAccessReader reader = new ByteArrayReader(values);
+
+        try
+        {
+            int val1 = reader.getUInt16(0);
+            int val2 = reader.getUInt16(2);
+
+            if (val1 == -1 && val2 == 1)
+                return "Slim Low";
+            if (val1 == -3 && val2 == 2)
+                return "Slim High";
+            if (val1 == 0 && val2 == 0)
+                return "Off";
+            if (val1 == 1 && val2 == 1)
+                return "Stretch Low";
+            if (val1 == 3 && val2 == 2)
+                return "Stretch High";
+
+            return "Unknown (" + val1 + " " + val2 + ")";
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getIntelligentExposureDescription()
+    {
+        return getIndexedDescription(TAG_INTELLIGENT_EXPOSURE,
+            "Off", "Low", "Standard", "High");
+    }
+
+    @Nullable
+    public String getFlashWarningDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_WARNING,
+            "No", "Yes (Flash required but disabled)");
+    }
+
+    @Nullable
+    public String getCountryDescription()
+    {
+        return getAsciiStringFromBytes(TAG_COUNTRY);
+    }
+
+    @Nullable
+    public String getStateDescription()
+    {
+        return getAsciiStringFromBytes(TAG_STATE);
+    }
+
+    @Nullable
+    public String getCityDescription()
+    {
+        return getAsciiStringFromBytes(TAG_CITY);
+    }
+
+    @Nullable
+    public String getLandmarkDescription()
+    {
+        return getAsciiStringFromBytes(TAG_LANDMARK);
+    }
+
+	@Nullable
+    public String getTitleDescription()
+    {
+        return getAsciiStringFromBytes(TAG_TITLE);
+    }
+
+	@Nullable
+    public String getBabyNameDescription()
+    {
+        return getAsciiStringFromBytes(TAG_BABY_NAME);
+    }
+
+	@Nullable
+    public String getLocationDescription()
+    {
+        return getAsciiStringFromBytes(TAG_LOCATION);
+    }
+
+    @Nullable
+    public String getIntelligentResolutionDescription()
+    {
+        return getIndexedDescription(TAG_INTELLIGENT_RESOLUTION,
+            "Off", null, "Auto", "On");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "Normal");
+    }
+
+    @Nullable
+    public String getWorldTimeLocationDescription()
+    {
+        return getIndexedDescription(TAG_WORLD_TIME_LOCATION,
+            1, "Home", "Destination");
+    }
+
+    @Nullable
+    public String getAdvancedSceneModeDescription()
+    {
+        return getIndexedDescription(TAG_ADVANCED_SCENE_MODE,
+            1,
+            "Normal",
+            "Outdoor/Illuminations/Flower/HDR Art",
+            "Indoor/Architecture/Objects/HDR B&W",
+            "Creative",
+            "Auto",
+            null,
+            "Expressive",
+            "Retro",
+            "Pure",
+            "Elegant",
+            null,
+            "Monochrome",
+            "Dynamic Art",
+            "Silhouette"
+        );
+    }
+
+    @Nullable
+    public String getUnknownDataDumpDescription()
+    {
+        return getByteLengthDescription(TAG_UNKNOWN_DATA_DUMP);
+    }
+
+    @Nullable
+    public String getColorEffectDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_EFFECT,
+            1, "Off", "Warm", "Cool", "Black & White", "Sepia"
+        );
+    }
+
+    @Nullable
+    public String getUptimeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_UPTIME);
+        if (value == null)
+            return null;
+        return value / 100f + " s";
+    }
+
+    @Nullable
+    public String getBurstModeDescription()
+    {
+        return getIndexedDescription(TAG_BURST_MODE,
+            "Off", null, "On", "Indefinite", "Unlimited"
+        );
+    }
+
+    @Nullable
+    public String getContrastModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_CONTRAST_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x0: return "Normal";
+            case 0x1: return "Low";
+            case 0x2: return "High";
+            case 0x6: return "Medium Low";
+            case 0x7: return "Medium High";
+            case 0x100: return "Low";
+            case 0x110: return "Normal";
+            case 0x120: return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNoiseReductionDescription()
+    {
+        return getIndexedDescription(TAG_NOISE_REDUCTION,
+            "Standard (0)", "Low (-1)", "High (+1)", "Lowest (-2)", "Highest (+2)"
+        );
+    }
+
+    @Nullable
+    public String getSelfTimerDescription()
+    {
+        return getIndexedDescription(TAG_SELF_TIMER,
+            1, "Off", "10 s", "2 s"
+        );
+    }
+
+    @Nullable
+    public String getRotationDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ROTATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Horizontal";
+            case 3: return "Rotate 180";
+            case 6: return "Rotate 90 CW";
+            case 8: return "Rotate 270 CW";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAfAssistLampDescription()
+    {
+        return getIndexedDescription(TAG_AF_ASSIST_LAMP,
+            1, "Fired", "Enabled but not used", "Disabled but required", "Disabled and not required"
+        );
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_MODE,
+            "Normal", "Natural", "Vivid"
+        );
+    }
+
+    @Nullable
+    public String getOpticalZoomModeDescription()
+    {
+        return getIndexedDescription(TAG_OPTICAL_ZOOM_MODE,
+            1, "Standard", "Extended"
+        );
+    }
+
+    @Nullable
+    public String getConversionLensDescription()
+    {
+        return getIndexedDescription(TAG_CONVERSION_LENS,
+            1, "Off", "Wide", "Telephoto", "Macro"
+        );
+    }
+
+    @Nullable
+    public String getDetectedFacesDescription()
+    {
+        return buildFacesDescription(_directory.getDetectedFaces());
+    }
+
+    @Nullable
+    public String getRecognizedFacesDescription()
+    {
+        return buildFacesDescription(_directory.getRecognizedFaces());
+    }
+
+    @Nullable
+    private String buildFacesDescription(@Nullable Face[] faces)
+    {
+        if (faces == null)
+            return null;
+
+        StringBuilder result = new StringBuilder();
+
+        for (int i = 0; i < faces.length; i++)
+            result.append("Face ").append(i + 1).append(": ").append(faces[i].toString()).append("\n");
+
+        return result.length() > 0 ? result.substring(0, result.length() - 1) : null;
+
+    }
+
+    private static final String[] _sceneModes = new String[] {
+        "Normal", // 1
+        "Portrait",
+        "Scenery",
+        "Sports",
+        "Night Portrait",
+        "Program",
+        "Aperture Priority",
+        "Shutter Priority",
+        "Macro",
+        "Spot", // 10
+        "Manual",
+        "Movie Preview",
+        "Panning",
+        "Simple",
+        "Color Effects",
+        "Self Portrait",
+        "Economy",
+        "Fireworks",
+        "Party",
+        "Snow", // 20
+        "Night Scenery",
+        "Food",
+        "Baby",
+        "Soft Skin",
+        "Candlelight",
+        "Starry Night",
+        "High Sensitivity",
+        "Panorama Assist",
+        "Underwater",
+        "Beach", // 30
+        "Aerial Photo",
+        "Sunset",
+        "Pet",
+        "Intelligent ISO",
+        "Clipboard",
+        "High Speed Continuous Shooting",
+        "Intelligent Auto",
+        null,
+        "Multi-aspect",
+        null, // 40
+        "Transform",
+        "Flash Burst",
+        "Pin Hole",
+        "Film Grain",
+        "My Color",
+        "Photo Frame",
+        null,
+        null,
+        null,
+        null, // 50
+        "HDR"
+    };
+
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        return getIndexedDescription(TAG_RECORD_MODE, 1, _sceneModes);
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_MODE, 1, _sceneModes);
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, 1,
+            "Auto", "Manual", null, "Auto, Focus Button", "Auto, Continuous");
+    }
+
+    @Nullable
+    public String getAfAreaModeDescription()
+    {
+        int[] value = _directory.getIntArray(TAG_AF_AREA_MODE);
+        if (value == null || value.length < 2)
+            return null;
+        switch (value[0]) {
+            case 0:
+                switch (value[1]) {
+                    case 1: return "Spot Mode On";
+                    case 16: return "Spot Mode Off";
+                    default: return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 1:
+                switch (value[1]) {
+                    case 0: return "Spot Focusing";
+                    case 1: return "5-area";
+                    default: return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 16:
+                switch (value[1]) {
+                    case 0: return "1-area";
+                    case 16: return "1-area (high speed)";
+                    default: return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 32:
+                switch (value[1]) {
+                    case 0: return "Auto or Face Detect";
+                    case 1: return "3-area (left)";
+                    case 2: return "3-area (center)";
+                    case 3: return "3-area (right)";
+                    default: return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 64: return "Face Detect";
+            default: return "Unknown (" + value[0] + " " + value[1] + ")";
+        }
+    }
+
+    @Nullable
+    public String getQualityModeDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY_MODE,
+            2,
+            "High", // 2
+            "Normal",
+            null,
+            null,
+            "Very High",
+            "Raw",
+            null,
+            "Motion Picture" // 9
+        );
+    }
+
+    @Nullable
+    public String getVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FIRMWARE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_VERSION, 2);
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
+    }
+
+    @Nullable
+    public String getInternalSerialNumberDescription()
+    {
+        return get7BitStringFromBytes(TAG_INTERNAL_SERIAL_NUMBER);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            1,
+            "Auto", // 1
+            "Daylight",
+            "Cloudy",
+            "Incandescent",
+            "Manual",
+            null,
+            null,
+            "Flash",
+            null,
+            "Black & White", // 10
+            "Manual",
+            "Shade" // 12
+        );
+    }
+
+	@Nullable
+	public String getBabyAgeDescription()
+    {
+        final Age age = _directory.getAge(TAG_BABY_AGE);
+        return age == null ? null : age.toFriendlyString();
+    }
+
+	@Nullable
+	public String getBabyAge1Description()
+    {
+        final Age age = _directory.getAge(TAG_BABY_AGE_1);
+        return age == null ? null : age.toFriendlyString();
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
new file mode 100644
index 0000000..4212bd5
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Age;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Face;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Panasonic and Leica cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Philipp Sandhaus
+ */
+public class PanasonicMakernoteDirectory extends Directory
+{
+
+    /**
+     * <br>
+     * 2 = High            <br>
+     * 3 = Normal          <br>
+     * 6 = Very High       <br>
+     * 7 = Raw             <br>
+     * 9 = Motion Picture  <br>
+     */
+    public static final int TAG_QUALITY_MODE = 0x0001;
+    public static final int TAG_FIRMWARE_VERSION = 0x0002;
+
+    /**
+     * <br>
+     * 1 = Auto            <br>
+     * 2 = Daylight        <br>
+     * 3 = Cloudy          <br>
+     * 4 = Incandescent    <br>
+     * 5 = Manual          <br>
+     * 8 = Flash           <br>
+     * 10 = Black & White  <br>
+     * 11 = Manual         <br>
+     * 12 = Shade          <br>
+     */
+    public static final int TAG_WHITE_BALANCE = 0x0003;
+
+
+    /**
+     * <br>
+     * 1 = Auto                <br>
+     * 2 = Manual              <br>
+     * 4 =  Auto, Focus Button <br>
+     * 5 = Auto, Continuous    <br>
+     */
+    public static final int TAG_FOCUS_MODE = 0x0007;
+
+    /**
+     * <br>
+     * 2 bytes                         <br>
+     * (DMC-FZ10)                      <br>
+     * '0 1' = Spot Mode On            <br>
+     * '0 16' = Spot Mode Off          <br>
+     * '(other models)                 <br>
+     * 16 = Normal?                    <br>
+     * '0 1' = 9-area                  <br>
+     * '0 16' = 3-area (high speed)    <br>
+     * '1 0' = Spot Focusing           <br>
+     * '1 1' = 5-area                  <br>
+     * '16 0' = 1-area                 <br>
+     * '16 16' = 1-area (high speed)   <br>
+     * '32 0' = Auto or Face Detect    <br>
+     * '32 1' = 3-area (left)?         <br>
+     * '32 2' = 3-area (center)?       <br>
+     * '32 3' = 3-area (right)?        <br>
+     * '64 0' = Face Detect            <br>
+     */
+    public static final int TAG_AF_AREA_MODE = 0x000f;
+
+    /**
+     * <br>
+     * 2 = On, Mode 1   <br>
+     * 3 = Off          <br>
+     * 4 = On, Mode 2   <br>
+     */
+    public static final int TAG_IMAGE_STABILIZATION = 0x001a;
+
+    /**
+     * <br>
+     * 1 = On    <br>
+     * 2 = Off   <br>
+     */
+    public static final int TAG_MACRO_MODE = 0x001C;
+
+    /**
+     * <br>
+     * 1 = Normal                            <br>
+     * 2 = Portrait                          <br>
+     * 3 = Scenery                           <br>
+     * 4 = Sports                            <br>
+     * 5 = Night Portrait                    <br>
+     * 6 = Program                           <br>
+     * 7 = Aperture Priority                 <br>
+     * 8 = Shutter Priority                  <br>
+     * 9 = Macro                             <br>
+     * 10= Spot                              <br>
+     * 11= Manual                            <br>
+     * 12= Movie Preview                     <br>
+     * 13= Panning                           <br>
+     * 14= Simple                            <br>
+     * 15= Color Effects                     <br>
+     * 16= Self Portrait                     <br>
+     * 17= Economy                           <br>
+     * 18= Fireworks                         <br>
+     * 19= Party                             <br>
+     * 20= Snow                              <br>
+     * 21= Night Scenery                     <br>
+     * 22= Food                              <br>
+     * 23= Baby                              <br>
+     * 24= Soft Skin                         <br>
+     * 25= Candlelight                       <br>
+     * 26= Starry Night                      <br>
+     * 27= High Sensitivity                  <br>
+     * 28= Panorama Assist                   <br>
+     * 29= Underwater                        <br>
+     * 30= Beach                             <br>
+     * 31= Aerial Photo                      <br>
+     * 32= Sunset                            <br>
+     * 33= Pet                               <br>
+     * 34= Intelligent ISO                   <br>
+     * 35= Clipboard                         <br>
+     * 36= High Speed Continuous Shooting    <br>
+     * 37= Intelligent Auto                  <br>
+     * 39= Multi-aspect                      <br>
+     * 41= Transform                         <br>
+     * 42= Flash Burst                       <br>
+     * 43= Pin Hole                          <br>
+     * 44= Film Grain                        <br>
+     * 45= My Color                          <br>
+     * 46= Photo Frame                       <br>
+     * 51= HDR                               <br>
+     */
+    public static final int TAG_RECORD_MODE = 0x001F;
+
+    /**
+     * 1 = Yes <br>
+     * 2 = No  <br>
+     */
+    public static final int TAG_AUDIO = 0x0020;
+
+    /**
+     * No idea, what this is
+     */
+    public static final int TAG_UNKNOWN_DATA_DUMP = 0x0021;
+
+    public static final int TAG_EASY_MODE = 0x0022;
+    public static final int TAG_WHITE_BALANCE_BIAS = 0x0023;
+    public static final int TAG_FLASH_BIAS = 0x0024;
+
+    /**
+     * this number is unique, and contains the date of manufacture,
+     * but is not the same as the number printed on the camera body
+     */
+    public static final int TAG_INTERNAL_SERIAL_NUMBER = 0x0025;
+
+    /**
+     * Panasonic Exif Version
+     */
+    public static final int TAG_EXIF_VERSION = 0x0026;
+
+
+    /**
+     * 1 = Off           <br>
+     * 2 = Warm          <br>
+     * 3 = Cool          <br>
+     * 4 = Black & White <br>
+     * 5 = Sepia         <br>
+     */
+    public static final int TAG_COLOR_EFFECT = 0x0028;
+
+    /**
+     * 4 Bytes <br>
+     * Time in 1/100 s from when the camera was powered on to when the
+     * image is written to memory card
+     */
+    public static final int TAG_UPTIME = 0x0029;
+
+
+    /**
+     * 0 = Off        <br>
+     * 1 = On         <br>
+     * 2 = Infinite   <br>
+     * 4 = Unlimited  <br>
+     */
+    public static final int TAG_BURST_MODE = 0x002a;
+
+    public static final int TAG_SEQUENCE_NUMBER = 0x002b;
+
+    /**
+     * (this decoding seems to work for some models such as the LC1, LX2, FZ7, FZ8, FZ18 and FZ50, but may not be correct for other models such as the FX10, G1, L1, L10 and LC80) <br>
+     * 0x0 = Normal                                            <br>
+     * 0x1 = Low                                               <br>
+     * 0x2 = High                                              <br>
+     * 0x6 = Medium Low                                        <br>
+     * 0x7 = Medium High                                       <br>
+     * 0x100 = Low                                             <br>
+     * 0x110 = Normal                                          <br>
+     * 0x120 = High                                            <br>
+     * (these values are used by the GF1)                      <br>
+     * 0 = -2                                                  <br>
+     * 1 = -1                                                  <br>
+     * 2 = Normal                                              <br>
+     * 3 = +1                                                  <br>
+     * 4 = +2                                                  <br>
+     * 7 = Nature (Color Film)                                 <br>
+     * 12 = Smooth (Color Film) or Pure (My Color)             <br>
+     * 17 = Dynamic (B&W Film)                                 <br>
+     * 22 = Smooth (B&W Film)                                  <br>
+     * 27 = Dynamic (Color Film)                               <br>
+     * 32 = Vibrant (Color Film) or Expressive (My Color)      <br>
+     * 33 = Elegant (My Color)                                 <br>
+     * 37 = Nostalgic (Color Film)                             <br>
+     * 41 = Dynamic Art (My Color)                             <br>
+     * 42 = Retro (My Color)                                   <br>
+     */
+    public static final int TAG_CONTRAST_MODE = 0x002c;
+
+
+    /**
+     * 0 = Standard      <br>
+     * 1 = Low (-1)      <br>
+     * 2 = High (+1)     <br>
+     * 3 = Lowest (-2)   <br>
+     * 4 = Highest (+2)  <br>
+     */
+    public static final int TAG_NOISE_REDUCTION = 0x002d;
+
+    /**
+     * 1 = Off   <br>
+     * 2 = 10 s  <br>
+     * 3 = 2 s   <br>
+     */
+    public static final int TAG_SELF_TIMER = 0x002e;
+
+    /**
+     * 1 = 0 DG    <br>
+     * 3 = 180 DG  <br>
+     * 6 =  90 DG  <br>
+     * 8 = 270 DG  <br>
+     */
+    public static final int TAG_ROTATION = 0x0030;
+
+    /**
+     * 1 = Fired <br>
+     * 2 = Enabled nut not used <br>
+     * 3 = Disabled but required <br>
+     * 4 = Disabled and not required
+     */
+    public static final int TAG_AF_ASSIST_LAMP = 0x0031;
+
+    /**
+     * 0 = Normal <br>
+     * 1 = Natural<br>
+     * 2 = Vivid
+     *
+     */
+    public static final int TAG_COLOR_MODE = 0x0032;
+
+    public static final int TAG_BABY_AGE = 0x0033;
+
+    /**
+     *  1 = Standard <br>
+     *  2 = Extended
+     */
+    public static final int TAG_OPTICAL_ZOOM_MODE = 0x0034;
+
+    /**
+     * 1 = Off <br>
+     * 2 = Wide <br>
+     * 3 = Telephoto <br>
+     * 4 = Macro
+     */
+    public static final int TAG_CONVERSION_LENS = 0x0035;
+
+    public static final int TAG_TRAVEL_DAY = 0x0036;
+
+    /**
+     * 0 = Normal
+     */
+    public static final int TAG_CONTRAST = 0x0039;
+
+    /**
+     * <br>
+     * 1 = Home <br>
+     * 2 = Destination
+     */
+    public static final int TAG_WORLD_TIME_LOCATION = 0x003a;
+
+    /**
+     * 1 = Off   <br>
+     * 2 = On
+     */
+    public static final int TAG_TEXT_STAMP = 0x003b;
+
+	public static final int TAG_PROGRAM_ISO = 0x003c;
+
+    /**
+     * <br>
+     * 1 = Normal                               <br>
+     * 2 = Outdoor/Illuminations/Flower/HDR Art <br>
+     * 3 = Indoor/Architecture/Objects/HDR B&W  <br>
+     * 4 = Creative                             <br>
+     * 5 = Auto                                 <br>
+     * 7 = Expressive                           <br>
+     * 8 = Retro                                <br>
+     * 9 = Pure                                 <br>
+     * 10 = Elegant                             <br>
+     * 12 = Monochrome                          <br>
+     * 13 = Dynamic Art                         <br>
+     * 14 = Silhouette                          <br>
+     */
+    public static final int TAG_ADVANCED_SCENE_MODE = 0x003d;
+
+    /**
+     * 1 = Off   <br>
+     * 2 = On
+     */
+    public static final int TAG_TEXT_STAMP_1 = 0x003e;
+
+    public static final int TAG_FACES_DETECTED = 0x003f;
+
+    public static final int TAG_SATURATION = 0x0040;
+    public static final int TAG_SHARPNESS = 0x0041;
+    public static final int TAG_FILM_MODE = 0x0042;
+
+    /**
+	 * WB adjust AB. Positive is a shift toward blue.
+	 */
+	public static final int TAG_WB_ADJUST_AB = 0x0046;
+    /**
+	 * WB adjust GM. Positive is a shift toward green.
+	 */
+	public static final int TAG_WB_ADJUST_GM = 0x0047;
+
+
+    public static final int TAG_AF_POINT_POSITION = 0x004d;
+
+
+    /**
+     * <br>
+     * Integer (16Bit) Indexes:                                             <br>
+     * 0  Number Face Positions (maybe less than Faces Detected)            <br>
+     * 1-4 Face Position 1                                                  <br>
+     * 5-8 Face Position 2                                                  <br>
+     * and so on                                                            <br>
+     *                                                                      <br>
+     * The four Integers are interpreted as follows:                        <br>
+     * (XYWH)  X,Y Center of Face,  (W,H) Width and Height                  <br>
+     * All values are in respect to double the size of the thumbnail image  <br>
+     *
+     */
+    public static final int TAG_FACE_DETECTION_INFO = 0x004e;
+    public static final int TAG_LENS_TYPE = 0x0051;
+    public static final int TAG_LENS_SERIAL_NUMBER = 0x0052;
+    public static final int TAG_ACCESSORY_TYPE = 0x0053;
+
+    /**
+     * (decoded as two 16-bit signed integers)
+     * '-1 1' = Slim Low
+     * '-3 2' = Slim High
+     * '0 0' = Off
+     * '1 1' = Stretch Low
+     * '3 2' = Stretch High
+     */
+    public static final int TAG_TRANSFORM = 0x0059;
+
+    /**
+    * 0 = Off <br>
+    * 1 = Low <br>
+    * 2 = Standard <br>
+    * 3 = High
+    */
+    public static final int TAG_INTELLIGENT_EXPOSURE = 0x005d;
+
+    /**
+	  * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+	public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    /**
+     * Byte Indexes:                                                                       <br>
+     *  0    Int (2  Byte) Number of Recognized Faces                                      <br>
+     *  4    String(20 Byte)    Recognized Face 1 Name                                     <br>
+     * 24    4 Int (8 Byte)     Recognized Face 1 Position  (Same Format as Face Detection)  <br>
+     * 32    String(20 Byte)    Recognized Face 1 Age                                      <br>
+     * 52    String(20 Byte)    Recognized Face 2 Name                                     <br>
+     * 72    4 Int (8 Byte)     Recognized Face 2 Position  (Same Format as Face Detection)  <br>
+     * 80    String(20 Byte)    Recognized Face 2 Age                                      <br>
+     *                                                                                     <br>
+     * And so on                                                                           <br>
+     *                                                                                     <br>
+     * The four Integers are interpreted as follows:                                       <br>
+     * (XYWH)  X,Y Center of Face,  (W,H) Width and Height                                 <br>
+     * All values are in respect to double the size of the thumbnail image                 <br>
+     *
+     */
+    public static final int TAG_FACE_RECOGNITION_INFO = 0x0061;
+
+    /**
+    * 0 = No <br>
+    * 1 = Yes
+    */
+    public static final int TAG_FLASH_WARNING = 0x0062;
+    public static final int TAG_RECOGNIZED_FACE_FLAGS = 0x0063;
+    public static final int TAG_TITLE = 0x0065;
+	public static final int TAG_BABY_NAME = 0x0066;
+	public static final int TAG_LOCATION = 0x0067;
+	public static final int TAG_COUNTRY = 0x0069;
+    public static final int TAG_STATE = 0x006b;
+    public static final int TAG_CITY = 0x006d;
+    public static final int TAG_LANDMARK = 0x006f;
+
+    /**
+     * 0 = Off <br>
+     * 2 = Auto <br>
+     * 3 = On
+     */
+    public static final int TAG_INTELLIGENT_RESOLUTION = 0x0070;
+
+    public static final int TAG_MAKERNOTE_VERSION = 0x8000;
+    public static final int TAG_SCENE_MODE = 0x8001;
+    public static final int TAG_WB_RED_LEVEL = 0x8004;
+    public static final int TAG_WB_GREEN_LEVEL = 0x8005;
+    public static final int TAG_WB_BLUE_LEVEL = 0x8006;
+    public static final int TAG_FLASH_FIRED = 0x8007;
+    public static final int TAG_TEXT_STAMP_2 = 0x8008;
+	public static final int TAG_TEXT_STAMP_3 = 0x8009;
+	public static final int TAG_BABY_AGE_1 = 0x8010;
+
+	/**
+     * (decoded as two 16-bit signed integers)
+     * '-1 1' = Slim Low
+     * '-3 2' = Slim High
+     * '0 0' = Off
+     * '1 1' = Stretch Low
+     * '3 2' = Stretch High
+     */
+    public static final int TAG_TRANSFORM_1 = 0x8012;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_QUALITY_MODE, "Quality Mode");
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Version");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_AF_AREA_MODE, "AF Area Mode");
+        _tagNameMap.put(TAG_IMAGE_STABILIZATION, "Image Stabilization");
+        _tagNameMap.put(TAG_MACRO_MODE, "Macro Mode");
+        _tagNameMap.put(TAG_RECORD_MODE, "Record Mode");
+        _tagNameMap.put(TAG_AUDIO, "Audio");
+        _tagNameMap.put(TAG_INTERNAL_SERIAL_NUMBER, "Internal Serial Number");
+        _tagNameMap.put(TAG_UNKNOWN_DATA_DUMP, "Unknown Data Dump");
+        _tagNameMap.put(TAG_EASY_MODE, "Easy Mode");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_EXIF_VERSION, "Exif Version");
+        _tagNameMap.put(TAG_COLOR_EFFECT, "Color Effect");
+        _tagNameMap.put(TAG_UPTIME, "Camera Uptime");
+        _tagNameMap.put(TAG_BURST_MODE, "Burst Mode");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(TAG_CONTRAST_MODE, "Contrast Mode");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_ROTATION, "Rotation");
+        _tagNameMap.put(TAG_AF_ASSIST_LAMP, "AF Assist Lamp");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_BABY_AGE, "Baby Age");
+        _tagNameMap.put(TAG_OPTICAL_ZOOM_MODE, "Optical Zoom Mode");
+        _tagNameMap.put(TAG_CONVERSION_LENS, "Conversion Lens");
+        _tagNameMap.put(TAG_TRAVEL_DAY, "Travel Day");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_WORLD_TIME_LOCATION, "World Time Location");
+        _tagNameMap.put(TAG_TEXT_STAMP, "Text Stamp");
+        _tagNameMap.put(TAG_PROGRAM_ISO, "Program ISO");
+		_tagNameMap.put(TAG_ADVANCED_SCENE_MODE, "Advanced Scene Mode");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_FACES_DETECTED, "Number of Detected Faces");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_FILM_MODE, "Film Mode");
+        _tagNameMap.put(TAG_WB_ADJUST_AB, "White Balance Adjust (AB)");
+		_tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)");
+		_tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position");
+        _tagNameMap.put(TAG_FACE_DETECTION_INFO, "Face Detection Info");
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        _tagNameMap.put(TAG_ACCESSORY_TYPE, "Accessory Type");
+        _tagNameMap.put(TAG_TRANSFORM, "Transform");
+        _tagNameMap.put(TAG_INTELLIGENT_EXPOSURE, "Intelligent Exposure");
+        _tagNameMap.put(TAG_FACE_RECOGNITION_INFO, "Face Recognition Info");
+        _tagNameMap.put(TAG_FLASH_WARNING, "Flash Warning");
+        _tagNameMap.put(TAG_RECOGNIZED_FACE_FLAGS, "Recognized Face Flags");
+		_tagNameMap.put(TAG_TITLE, "Title");
+		_tagNameMap.put(TAG_BABY_NAME, "Baby Name");
+		_tagNameMap.put(TAG_LOCATION, "Location");
+		_tagNameMap.put(TAG_COUNTRY, "Country");
+        _tagNameMap.put(TAG_STATE, "State");
+        _tagNameMap.put(TAG_CITY, "City");
+        _tagNameMap.put(TAG_LANDMARK, "Landmark");
+        _tagNameMap.put(TAG_INTELLIGENT_RESOLUTION, "Intelligent Resolution");
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_WB_RED_LEVEL, "White Balance (Red)");
+        _tagNameMap.put(TAG_WB_GREEN_LEVEL, "White Balance (Green)");
+        _tagNameMap.put(TAG_WB_BLUE_LEVEL, "White Balance (Blue)");
+        _tagNameMap.put(TAG_FLASH_FIRED, "Flash Fired");
+		_tagNameMap.put(TAG_TEXT_STAMP_1, "Text Stamp 1");
+		_tagNameMap.put(TAG_TEXT_STAMP_2, "Text Stamp 2");
+		_tagNameMap.put(TAG_TEXT_STAMP_3, "Text Stamp 3");
+		_tagNameMap.put(TAG_BABY_AGE_1, "Baby Age 1");
+		_tagNameMap.put(TAG_TRANSFORM_1, "Transform 1");
+    }
+
+    public PanasonicMakernoteDirectory()
+    {
+        this.setDescriptor(new PanasonicMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Panasonic Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    @Nullable
+    public Face[] getDetectedFaces()
+    {
+        byte[] bytes = getByteArray(TAG_FACE_DETECTION_INFO);
+        if (bytes==null)
+            return null;
+
+        RandomAccessReader reader = new ByteArrayReader(bytes);
+        reader.setMotorolaByteOrder(false);
+
+        try {
+            int faceCount = reader.getUInt16(0);
+            if (faceCount==0)
+                return null;
+            Face[] faces = new Face[faceCount];
+
+            for (int i = 0; i < faceCount; i++) {
+                int offset = 2 + i * 8;
+                faces[i] = new Face(
+                        reader.getUInt16(offset),
+                        reader.getUInt16(offset + 2),
+                        reader.getUInt16(offset + 4),
+                        reader.getUInt16(offset + 6)
+                        , null, null);
+            }
+            return faces;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public Face[] getRecognizedFaces()
+    {
+        byte[] bytes = getByteArray(TAG_FACE_RECOGNITION_INFO);
+        if (bytes == null)
+            return null;
+
+        RandomAccessReader reader = new ByteArrayReader(bytes);
+        reader.setMotorolaByteOrder(false);
+
+        try {
+            int faceCount = reader.getUInt16(0);
+            if (faceCount==0)
+                return null;
+            Face[] faces = new Face[faceCount];
+
+            for (int i = 0; i < faceCount; i++) {
+                int offset = 4 + i * 44;
+                String name = reader.getString(offset, 20, "ASCII").trim();
+                String age = reader.getString(offset + 28, 20, "ASCII").trim();
+                faces[i] = new Face(
+                        reader.getUInt16(offset + 20),
+                        reader.getUInt16(offset + 22),
+                        reader.getUInt16(offset + 24),
+                        reader.getUInt16(offset + 26),
+                        name,
+                        Age.fromPanasonicString(age));
+            }
+            return faces;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Attempts to convert the underlying string value (as stored in the directory) into an Age object.
+     * @param tag The tag identifier.
+     * @return The parsed Age object, or null if the tag was empty of the value unable to be parsed.
+     */
+	@Nullable
+	public Age getAge(int tag)
+    {
+        final String ageString = getString(tag);
+        if (ageString==null)
+            return null;
+        return Age.fromPanasonicString(ageString);
+	}
+}
diff --git a/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
new file mode 100644
index 0000000..30889e1
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.PentaxMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PentaxMakernoteDirectory}.
+ * <p>
+ * Some information about this makernote taken from here:
+ * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pentax_mn.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PentaxMakernoteDescriptor extends TagDescriptor<PentaxMakernoteDirectory>
+{
+    public PentaxMakernoteDescriptor(@NotNull PentaxMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_CAPTURE_MODE:
+                return getCaptureModeDescription();
+            case TAG_QUALITY_LEVEL:
+                return getQualityLevelDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_ISO_SPEED:
+                return getIsoSpeedDescription();
+            case TAG_COLOUR:
+                return getColourDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getColourDescription()
+    {
+        return getIndexedDescription(TAG_COLOUR, 1, "Normal", "Black & White", "Sepia");
+    }
+
+    @Nullable
+    public String getIsoSpeedDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ISO_SPEED);
+        if (value == null)
+            return null;
+        switch (value) {
+            // TODO there must be other values which aren't catered for here
+            case 10: return "ISO 100";
+            case 16: return "ISO 200";
+            case 100: return "ISO 100";
+            case 200: return "ISO 200";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "Normal", "Low", "High");
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS, "Normal", "Soft", "Hard");
+    }
+
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Float value = _directory.getFloatObject(TAG_DIGITAL_ZOOM);
+        if (value == null)
+            return null;
+        if (value == 0)
+            return "Off";
+        return Float.toString(value);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE,
+            "Auto", "Daylight", "Shade", "Tungsten", "Fluorescent", "Manual");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE,
+            1, "Auto", "Flash On", null, "Flash Off", null, "Red-eye Reduction");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE, 2, "Custom", "Auto");
+    }
+
+    @Nullable
+    public String getQualityLevelDescription()
+    {
+        return getIndexedDescription(TAG_QUALITY_LEVEL, "Good", "Better", "Best");
+    }
+
+    @Nullable
+    public String getCaptureModeDescription()
+    {
+        return getIndexedDescription(TAG_CAPTURE_MODE,
+            "Auto", "Night-scene", "Manual", null, "Multiple");
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
new file mode 100644
index 0000000..ee38d55
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Pentax and Asahi cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PentaxMakernoteDirectory extends Directory
+{
+    /**
+     * 0 = Auto
+     * 1 = Night-scene
+     * 2 = Manual
+     * 4 = Multiple
+     */
+    public static final int TAG_CAPTURE_MODE = 0x0001;
+
+    /**
+     * 0 = Good
+     * 1 = Better
+     * 2 = Best
+     */
+    public static final int TAG_QUALITY_LEVEL = 0x0002;
+
+    /**
+     * 2 = Custom
+     * 3 = Auto
+     */
+    public static final int TAG_FOCUS_MODE = 0x0003;
+
+    /**
+     * 1 = Auto
+     * 2 = Flash on
+     * 4 = Flash off
+     * 6 = Red-eye Reduction
+     */
+    public static final int TAG_FLASH_MODE = 0x0004;
+
+    /**
+     * 0 = Auto
+     * 1 = Daylight
+     * 2 = Shade
+     * 3 = Tungsten
+     * 4 = Fluorescent
+     * 5 = Manual
+     */
+    public static final int TAG_WHITE_BALANCE = 0x0007;
+
+    /**
+     * (0 = Off)
+     */
+    public static final int TAG_DIGITAL_ZOOM = 0x000A;
+
+    /**
+     * 0 = Normal
+     * 1 = Soft
+     * 2 = Hard
+     */
+    public static final int TAG_SHARPNESS = 0x000B;
+
+    /**
+     * 0 = Normal
+     * 1 = Low
+     * 2 = High
+     */
+    public static final int TAG_CONTRAST = 0x000C;
+
+    /**
+     * 0 = Normal
+     * 1 = Low
+     * 2 = High
+     */
+    public static final int TAG_SATURATION = 0x000D;
+
+    /**
+     * 10 = ISO 100
+     * 16 = ISO 200
+     * 100 = ISO 100
+     * 200 = ISO 200
+     */
+    public static final int TAG_ISO_SPEED = 0x0014;
+
+    /**
+     * 1 = Normal
+     * 2 = Black & White
+     * 3 = Sepia
+     */
+    public static final int TAG_COLOUR = 0x0017;
+
+    /**
+     * See Print Image Matching for specification.
+     * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+     */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    /**
+     * (String).
+     */
+    public static final int TAG_TIME_ZONE = 0x1000;
+
+    /**
+     * (String).
+     */
+    public static final int TAG_DAYLIGHT_SAVINGS = 0x1001;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CAPTURE_MODE, "Capture Mode");
+        _tagNameMap.put(TAG_QUALITY_LEVEL, "Quality Level");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_ISO_SPEED, "ISO Speed");
+        _tagNameMap.put(TAG_COLOUR, "Colour");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_TIME_ZONE, "Time Zone");
+        _tagNameMap.put(TAG_DAYLIGHT_SAVINGS, "Daylight Savings");
+    }
+
+    public PentaxMakernoteDirectory()
+    {
+        this.setDescriptor(new PentaxMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Pentax Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
new file mode 100644
index 0000000..46001db
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDescriptor}.
+ * <p>
+ * Some information about this makernote taken from here:
+ * http://www.ozhiker.com/electronics/pjmt/jpeg_info/ricoh_mn.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirectory>
+{
+    public RicohMakernoteDescriptor(@NotNull RicohMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+//            case TAG_PRINT_IMAGE_MATCHING_INFO:
+//                return getPrintImageMatchingInfoDescription();
+//            case TAG_PROPRIETARY_THUMBNAIL:
+//                return getProprietaryThumbnailDataDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+//    @Nullable
+//    public String getPrintImageMatchingInfoDescription()
+//    {
+//        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
+//    }
+//
+//    @Nullable
+//    public String getProprietaryThumbnailDataDescription()
+//    {
+//        return getByteLengthDescription(TAG_PROPRIETARY_THUMBNAIL);
+//    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
new file mode 100644
index 0000000..fdb1e90
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Ricoh cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RicohMakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_DATA_TYPE = 0x0001;
+    public static final int TAG_VERSION = 0x0002;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    public static final int TAG_RICOH_CAMERA_INFO_MAKERNOTE_SUB_IFD_POINTER = 0x2001;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_DATA_TYPE, "Makernote Data Type");
+        _tagNameMap.put(TAG_VERSION, "Version");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_RICOH_CAMERA_INFO_MAKERNOTE_SUB_IFD_POINTER, "Ricoh Camera Info Makernote Sub-IFD");
+    }
+
+    public RicohMakernoteDirectory()
+    {
+        this.setDescriptor(new RicohMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Ricoh Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
new file mode 100644
index 0000000..22db4fe
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SanyoMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SanyoMakernoteDescriptor extends TagDescriptor<SanyoMakernoteDirectory>
+{
+    public SanyoMakernoteDescriptor(@NotNull SanyoMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_SANYO_QUALITY:
+                return getSanyoQualityDescription();
+            case TAG_MACRO:
+                return getMacroDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_SEQUENTIAL_SHOT:
+                return getSequentialShotDescription();
+            case TAG_WIDE_RANGE:
+                return getWideRangeDescription();
+            case TAG_COLOR_ADJUSTMENT_MODE:
+                return getColorAdjustmentModeDescription();
+            case TAG_QUICK_SHOT:
+                return getQuickShotDescription();
+            case TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case TAG_VOICE_MEMO:
+                return getVoiceMemoDescription();
+            case TAG_RECORD_SHUTTER_RELEASE:
+                return getRecordShutterDescription();
+            case TAG_FLICKER_REDUCE:
+                return getFlickerReduceDescription();
+            case TAG_OPTICAL_ZOOM_ON:
+                return getOptimalZoomOnDescription();
+            case TAG_DIGITAL_ZOOM_ON:
+                return getDigitalZoomOnDescription();
+            case TAG_LIGHT_SOURCE_SPECIAL:
+                return getLightSourceSpecialDescription();
+            case TAG_RESAVED:
+                return getResavedDescription();
+            case TAG_SCENE_SELECT:
+                return getSceneSelectDescription();
+            case TAG_SEQUENCE_SHOT_INTERVAL:
+                return getSequenceShotIntervalDescription();
+            case TAG_FLASH_MODE:
+                return getFlashModeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSanyoQualityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SANYO_QUALITY);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x0: return "Normal/Very Low";
+            case 0x1: return "Normal/Low";
+            case 0x2: return "Normal/Medium Low";
+            case 0x3: return "Normal/Medium";
+            case 0x4: return "Normal/Medium High";
+            case 0x5: return "Normal/High";
+            case 0x6: return "Normal/Very High";
+            case 0x7: return "Normal/Super High";
+            case 0x100: return "Fine/Very Low";
+            case 0x101: return "Fine/Low";
+            case 0x102: return "Fine/Medium Low";
+            case 0x103: return "Fine/Medium";
+            case 0x104: return "Fine/Medium High";
+            case 0x105: return "Fine/High";
+            case 0x106: return "Fine/Very High";
+            case 0x107: return "Fine/Super High";
+            case 0x200: return "Super Fine/Very Low";
+            case 0x201: return "Super Fine/Low";
+            case 0x202: return "Super Fine/Medium Low";
+            case 0x203: return "Super Fine/Medium";
+            case 0x204: return "Super Fine/Medium High";
+            case 0x205: return "Super Fine/High";
+            case 0x206: return "Super Fine/Very High";
+            case 0x207: return "Super Fine/Super High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    private String getMacroDescription()
+    {
+        return getIndexedDescription(TAG_MACRO, "Normal", "Macro", "View", "Manual");
+    }
+
+    @Nullable
+    private String getDigitalZoomDescription()
+    {
+        return getDecimalRational(TAG_DIGITAL_ZOOM, 3);
+    }
+
+    @Nullable
+    private String getSequentialShotDescription()
+    {
+        return getIndexedDescription(TAG_SEQUENTIAL_SHOT, "None", "Standard", "Best", "Adjust Exposure");
+    }
+
+    @Nullable
+    private String getWideRangeDescription()
+    {
+        return getIndexedDescription(TAG_WIDE_RANGE, "Off", "On");
+    }
+
+    @Nullable
+    private String getColorAdjustmentModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_ADJUSTMENT_MODE, "Off", "On");
+    }
+
+    @Nullable
+    private String getQuickShotDescription()
+    {
+        return getIndexedDescription(TAG_QUICK_SHOT, "Off", "On");
+    }
+
+    @Nullable
+    private String getSelfTimerDescription()
+    {
+        return getIndexedDescription(TAG_SELF_TIMER, "Off", "On");
+    }
+
+    @Nullable
+    private String getVoiceMemoDescription()
+    {
+        return getIndexedDescription(TAG_VOICE_MEMO, "Off", "On");
+    }
+
+    @Nullable
+    private String getRecordShutterDescription()
+    {
+        return getIndexedDescription(TAG_RECORD_SHUTTER_RELEASE, "Record while down", "Press start, press stop");
+    }
+
+    @Nullable
+    private String getFlickerReduceDescription()
+    {
+        return getIndexedDescription(TAG_FLICKER_REDUCE, "Off", "On");
+    }
+
+    @Nullable
+    private String getOptimalZoomOnDescription()
+    {
+        return getIndexedDescription(TAG_OPTICAL_ZOOM_ON, "Off", "On");
+    }
+
+    @Nullable
+    private String getDigitalZoomOnDescription()
+    {
+        return getIndexedDescription(TAG_DIGITAL_ZOOM_ON, "Off", "On");
+    }
+
+    @Nullable
+    private String getLightSourceSpecialDescription()
+    {
+        return getIndexedDescription(TAG_LIGHT_SOURCE_SPECIAL, "Off", "On");
+    }
+
+    @Nullable
+    private String getResavedDescription()
+    {
+        return getIndexedDescription(TAG_RESAVED, "No", "Yes");
+    }
+
+    @Nullable
+    private String getSceneSelectDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_SELECT,
+            "Off", "Sport", "TV", "Night", "User 1", "User 2", "Lamp");
+    }
+
+    @Nullable
+    private String getSequenceShotIntervalDescription()
+    {
+        return getIndexedDescription(TAG_SEQUENCE_SHOT_INTERVAL,
+            "5 frames/sec", "10 frames/sec", "15 frames/sec", "20 frames/sec");
+    }
+
+    @Nullable
+    private String getFlashModeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODE,
+            "Auto", "Force", "Disabled", "Red eye");
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
new file mode 100644
index 0000000..26500c6
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sanyo cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SanyoMakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_OFFSET = 0x00ff;
+
+    public static final int TAG_SANYO_THUMBNAIL = 0x0100;
+
+    public static final int TAG_SPECIAL_MODE = 0x0200;
+    public static final int TAG_SANYO_QUALITY = 0x0201;
+    public static final int TAG_MACRO = 0x0202;
+    public static final int TAG_DIGITAL_ZOOM = 0x0204;
+    public static final int TAG_SOFTWARE_VERSION = 0x0207;
+    public static final int TAG_PICT_INFO = 0x0208;
+    public static final int TAG_CAMERA_ID = 0x0209;
+    public static final int TAG_SEQUENTIAL_SHOT = 0x020e;
+    public static final int TAG_WIDE_RANGE = 0x020f;
+    public static final int TAG_COLOR_ADJUSTMENT_MODE = 0x0210;
+    public static final int TAG_QUICK_SHOT = 0x0213;
+    public static final int TAG_SELF_TIMER = 0x0214;
+    public static final int TAG_VOICE_MEMO = 0x0216;
+    public static final int TAG_RECORD_SHUTTER_RELEASE = 0x0217;
+    public static final int TAG_FLICKER_REDUCE = 0x0218;
+    public static final int TAG_OPTICAL_ZOOM_ON = 0x0219;
+    public static final int TAG_DIGITAL_ZOOM_ON = 0x021b;
+    public static final int TAG_LIGHT_SOURCE_SPECIAL = 0x021d;
+    public static final int TAG_RESAVED = 0x021e;
+    public static final int TAG_SCENE_SELECT = 0x021f;
+    public static final int TAG_MANUAL_FOCUS_DISTANCE_OR_FACE_INFO = 0x0223;
+    public static final int TAG_SEQUENCE_SHOT_INTERVAL = 0x0224;
+    public static final int TAG_FLASH_MODE = 0x0225;
+
+    public static final int TAG_PRINT_IM = 0x0e00;
+
+    public static final int TAG_DATA_DUMP = 0x0f00;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_OFFSET, "Makernote Offset");
+
+        _tagNameMap.put(TAG_SANYO_THUMBNAIL, "Sanyo Thumbnail");
+
+        _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode");
+        _tagNameMap.put(TAG_SANYO_QUALITY, "Sanyo Quality");
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_SOFTWARE_VERSION, "Software Version");
+        _tagNameMap.put(TAG_PICT_INFO, "Pict Info");
+        _tagNameMap.put(TAG_CAMERA_ID, "Camera ID");
+        _tagNameMap.put(TAG_SEQUENTIAL_SHOT, "Sequential Shot");
+        _tagNameMap.put(TAG_WIDE_RANGE, "Wide Range");
+        _tagNameMap.put(TAG_COLOR_ADJUSTMENT_MODE, "Color Adjustment Node");
+        _tagNameMap.put(TAG_QUICK_SHOT, "Quick Shot");
+        _tagNameMap.put(TAG_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_VOICE_MEMO, "Voice Memo");
+        _tagNameMap.put(TAG_RECORD_SHUTTER_RELEASE, "Record Shutter Release");
+        _tagNameMap.put(TAG_FLICKER_REDUCE, "Flicker Reduce");
+        _tagNameMap.put(TAG_OPTICAL_ZOOM_ON, "Optical Zoom On");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM_ON, "Digital Zoom On");
+        _tagNameMap.put(TAG_LIGHT_SOURCE_SPECIAL, "Light Source Special");
+        _tagNameMap.put(TAG_RESAVED, "Resaved");
+        _tagNameMap.put(TAG_SCENE_SELECT, "Scene Select");
+        _tagNameMap.put(TAG_MANUAL_FOCUS_DISTANCE_OR_FACE_INFO, "Manual Focus Distance or Face Info");
+        _tagNameMap.put(TAG_SEQUENCE_SHOT_INTERVAL, "Sequence Shot Interval");
+        _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode");
+
+        _tagNameMap.put(TAG_PRINT_IM, "Print IM");
+
+        _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
+    }
+
+    public SanyoMakernoteDirectory()
+    {
+        this.setDescriptor(new SanyoMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sanyo Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
new file mode 100644
index 0000000..56442f1
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SigmaMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SigmaMakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SigmaMakernoteDescriptor extends TagDescriptor<SigmaMakernoteDirectory>
+{
+    public SigmaMakernoteDescriptor(@NotNull SigmaMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case TAG_METERING_MODE:
+                return getMeteringModeDescription();
+        }
+        return super.getDescription(tagType);
+    }
+
+    @Nullable
+    private String getMeteringModeDescription()
+    {
+        String value = _directory.getString(TAG_METERING_MODE);
+        if (value == null || value.length() == 0)
+            return null;
+        switch (value.charAt(0)) {
+            case '8': return "Multi Segment";
+            case 'A': return "Average";
+            case 'C': return "Center Weighted Average";
+            default:
+                return value;
+        }
+    }
+
+    @Nullable
+    private String getExposureModeDescription()
+    {
+        String value = _directory.getString(TAG_EXPOSURE_MODE);
+        if (value == null || value.length() == 0)
+            return null;
+        switch (value.charAt(0)) {
+            case 'A': return "Aperture Priority AE";
+            case 'M': return "Manual";
+            case 'P': return "Program AE";
+            case 'S': return "Shutter Speed Priority AE";
+            default:
+                return value;
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
new file mode 100644
index 0000000..75afedf
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sigma / Foveon cameras.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SigmaMakernoteDirectory extends Directory
+{
+    public static final int TAG_SERIAL_NUMBER = 0x2;
+    public static final int TAG_DRIVE_MODE = 0x3;
+    public static final int TAG_RESOLUTION_MODE = 0x4;
+    public static final int TAG_AUTO_FOCUS_MODE = 0x5;
+    public static final int TAG_FOCUS_SETTING = 0x6;
+    public static final int TAG_WHITE_BALANCE = 0x7;
+    public static final int TAG_EXPOSURE_MODE = 0x8;
+    public static final int TAG_METERING_MODE = 0x9;
+    public static final int TAG_LENS_RANGE = 0xa;
+    public static final int TAG_COLOR_SPACE = 0xb;
+    public static final int TAG_EXPOSURE = 0xc;
+    public static final int TAG_CONTRAST = 0xd;
+    public static final int TAG_SHADOW = 0xe;
+    public static final int TAG_HIGHLIGHT = 0xf;
+    public static final int TAG_SATURATION = 0x10;
+    public static final int TAG_SHARPNESS = 0x11;
+    public static final int TAG_FILL_LIGHT = 0x12;
+    public static final int TAG_COLOR_ADJUSTMENT = 0x14;
+    public static final int TAG_ADJUSTMENT_MODE = 0x15;
+    public static final int TAG_QUALITY = 0x16;
+    public static final int TAG_FIRMWARE = 0x17;
+    public static final int TAG_SOFTWARE = 0x18;
+    public static final int TAG_AUTO_BRACKET = 0x19;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_DRIVE_MODE, "Drive Mode");
+        _tagNameMap.put(TAG_RESOLUTION_MODE, "Resolution Mode");
+        _tagNameMap.put(TAG_AUTO_FOCUS_MODE, "Auto Focus Mode");
+        _tagNameMap.put(TAG_FOCUS_SETTING, "Focus Setting");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(TAG_LENS_RANGE, "Lens Range");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_EXPOSURE, "Exposure");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SHADOW, "Shadow");
+        _tagNameMap.put(TAG_HIGHLIGHT, "Highlight");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_FILL_LIGHT, "Fill Light");
+        _tagNameMap.put(TAG_COLOR_ADJUSTMENT, "Color Adjustment");
+        _tagNameMap.put(TAG_ADJUSTMENT_MODE, "Adjustment Mode");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FIRMWARE, "Firmware");
+        _tagNameMap.put(TAG_SOFTWARE, "Software");
+        _tagNameMap.put(TAG_AUTO_BRACKET, "Auto Bracket");
+    }
+
+
+    public SigmaMakernoteDirectory()
+    {
+        this.setDescriptor(new SigmaMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sigma Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
new file mode 100644
index 0000000..8ae873f
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SonyType1MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SonyType1MakernoteDirectory}.
+ * Thanks to David Carson for the initial version of this class.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1MakernoteDirectory>
+{
+    public SonyType1MakernoteDescriptor(@NotNull SonyType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_IMAGE_QUALITY:
+                return getImageQualityDescription();
+            case TAG_FLASH_EXPOSURE_COMP:
+                return getFlashExposureCompensationDescription();
+            case TAG_TELECONVERTER:
+                return getTeleconverterDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_COLOR_TEMPERATURE:
+                return getColorTemperatureDescription();
+            case TAG_SCENE_MODE:
+                return getSceneModeDescription();
+            case TAG_ZONE_MATCHING:
+                return getZoneMatchingDescription();
+            case TAG_DYNAMIC_RANGE_OPTIMISER:
+                return getDynamicRangeOptimizerDescription();
+            case TAG_IMAGE_STABILISATION:
+                return getImageStabilizationDescription();
+            // Unfortunately it seems that there is no definite mapping between a lens ID and a lens model
+            // http://gvsoft.homedns.org/exif/makernote-sony-type1.html#0xb027
+//            case TAG_LENS_ID:
+//                return getLensIDDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_MACRO:
+                return getMacroDescription();
+            case TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case TAG_JPEG_QUALITY:
+                return getJpegQualityDescription();
+            case TAG_ANTI_BLUR:
+                return getAntiBlurDescription();
+            case TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE:
+                return getLongExposureNoiseReductionDescription();
+            case TAG_HIGH_ISO_NOISE_REDUCTION:
+                return getHighIsoNoiseReductionDescription();
+            case TAG_PICTURE_EFFECT:
+                return getPictureEffectDescription();
+            case TAG_SOFT_SKIN_EFFECT:
+                return getSoftSkinEffectDescription();
+            case TAG_VIGNETTING_CORRECTION:
+                return getVignettingCorrectionDescription();
+            case TAG_LATERAL_CHROMATIC_ABERRATION:
+                return getLateralChromaticAberrationDescription();
+            case TAG_DISTORTION_CORRECTION:
+                return getDistortionCorrectionDescription();
+            case TAG_AUTO_PORTRAIT_FRAMED:
+                return getAutoPortraitFramedDescription();
+            case TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case TAG_AF_POINT_SELECTED:
+                return getAFPointSelectedDescription();
+            case TAG_SONY_MODEL_ID:
+                return getSonyModelIdDescription();
+            case TAG_AF_MODE:
+                return getAFModeDescription();
+            case TAG_AF_ILLUMINATOR:
+                return getAFIlluminatorDescription();
+            case TAG_FLASH_LEVEL:
+                return getFlashLevelDescription();
+            case TAG_RELEASE_MODE:
+                return getReleaseModeDescription();
+            case TAG_SEQUENCE_NUMBER:
+                return getSequenceNumberDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getImageQualityDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_QUALITY,
+            "RAW",
+            "Super Fine",
+            "Fine",
+            "Standard",
+            "Economy",
+            "Extra Fine",
+            "RAW + JPEG",
+            "Compressed RAW",
+            "Compressed RAW + JPEG");
+    }
+
+    @Nullable
+    public String getFlashExposureCompensationDescription()
+    {
+        return getFormattedInt(TAG_FLASH_EXPOSURE_COMP, "%d EV");
+    }
+
+    @Nullable
+    public String getTeleconverterDescription()
+    {
+        Integer value = _directory.getInteger(TAG_TELECONVERTER);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "None";
+            case 0x48: return "Minolta/Sony AF 2x APO (D)";
+            case 0x50: return "Minolta AF 2x APO II";
+            case 0x60: return "Minolta AF 2x APO";
+            case 0x88: return "Minolta/Sony AF 1.4x APO (D)";
+            case 0x90: return "Minolta AF 1.4x APO II";
+            case 0xa0: return "Minolta AF 1.4x APO";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x00: return "Auto";
+            case 0x01: return "Color Temperature/Color Filter";
+            case 0x10: return "Daylight";
+            case 0x20: return "Cloudy";
+            case 0x30: return "Shade";
+            case 0x40: return "Tungsten";
+            case 0x50: return "Flash";
+            case 0x60: return "Fluorescent";
+            case 0x70: return "Custom";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorTemperatureDescription()
+    {
+        Integer value = _directory.getInteger(TAG_COLOR_TEMPERATURE);
+        if (value == null)
+            return null;
+        if (value == 0)
+            return "Auto";
+        int kelvin = ((value & 0x00FF0000) >> 8) | ((value & 0xFF000000) >> 24);
+        return String.format("%d K", kelvin);
+    }
+
+    @Nullable
+    public String getZoneMatchingDescription()
+    {
+        return getIndexedDescription(TAG_ZONE_MATCHING,
+            "ISO Setting Used", "High Key", "Low Key");
+    }
+
+    @Nullable
+    public String getDynamicRangeOptimizerDescription()
+    {
+        Integer value = _directory.getInteger(TAG_DYNAMIC_RANGE_OPTIMISER);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Standard";
+            case 2: return "Advanced Auto";
+            case 3: return "Auto";
+            case 8: return "Advanced LV1";
+            case 9: return "Advanced LV2";
+            case 10: return "Advanced LV3";
+            case 11: return "Advanced LV4";
+            case 12: return "Advanced LV5";
+            case 16: return "LV1";
+            case 17: return "LV2";
+            case 18: return "LV3";
+            case 19: return "LV4";
+            case 20: return "LV5";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        Integer value = _directory.getInteger(TAG_IMAGE_STABILISATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On";
+            default: return "N/A";
+        }
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_COLOR_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Standard";
+            case 1: return "Vivid";
+            case 2: return "Portrait";
+            case 3: return "Landscape";
+            case 4: return "Sunset";
+            case 5: return "Night Portrait";
+            case 6: return "Black & White";
+            case 7: return "Adobe RGB";
+            case 12: case 100: return "Neutral";
+            case 13: case 101: return "Clear";
+            case 14: case 102: return "Deep";
+            case 15: case 103: return "Light";
+            case 16: return "Autumn";
+            case 17: return "Sepia";
+            case 104: return "Night View";
+            case 105: return "Autumn Leaves";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getMacroDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MACRO);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On";
+            case 2: return "Magnifying Glass/Super Macro";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_EXPOSURE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Program";
+            case 1: return "Portrait";
+            case 2: return "Beach";
+            case 3: return "Sports";
+            case 4: return "Snow";
+            case 5: return "Landscape";
+            case 6: return "Auto";
+            case 7: return "Aperture Priority";
+            case 8: return "Shutter Priority";
+            case 9: return "Night Scene / Twilight";
+            case 10: return "Hi-Speed Shutter";
+            case 11: return "Twilight Portrait";
+            case 12: return "Soft Snap/Portrait";
+            case 13: return "Fireworks";
+            case 14: return "Smile Shutter";
+            case 15: return "Manual";
+            case 18: return "High Sensitivity";
+            case 19: return "Macro";
+            case 20: return "Advanced Sports Shooting";
+            case 29: return "Underwater";
+            case 33: return "Food";
+            case 34: return "Panorama";
+            case 35: return "Handheld Night Shot";
+            case 36: return "Anti Motion Blur";
+            case 37: return "Pet";
+            case 38: return "Backlight Correction HDR";
+            case 39: return "Superior Auto";
+            case 40: return "Background Defocus";
+            case 41: return "Soft Skin";
+            case 42: return "3D Image";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getJpegQualityDescription()
+    {
+        Integer value = _directory.getInteger(TAG_JPEG_QUALITY);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Normal";
+            case 1: return "Fine";
+            case 2: return "Extra Fine";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getAntiBlurDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ANTI_BLUR);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On (Continuous)";
+            case 2: return "On (Shooting)";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getLongExposureNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getHighIsoNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_HIGH_ISO_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "On";
+            case 2: return "Normal";
+            case 3: return "High";
+            case 0x100: return "Auto";
+            case 0xffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getPictureEffectDescription()
+    {
+        Integer value = _directory.getInteger(TAG_PICTURE_EFFECT);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Toy Camera";
+            case 2: return "Pop Color";
+            case 3: return "Posterization";
+            case 4: return "Posterization B/W";
+            case 5: return "Retro Photo";
+            case 6: return "Soft High Key";
+            case 7: return "Partial Color (red)";
+            case 8: return "Partial Color (green)";
+            case 9: return "Partial Color (blue)";
+            case 10: return "Partial Color (yellow)";
+            case 13: return "High Contrast Monochrome";
+            case 16: return "Toy Camera (normal)";
+            case 17: return "Toy Camera (cool)";
+            case 18: return "Toy Camera (warm)";
+            case 19: return "Toy Camera (green)";
+            case 20: return "Toy Camera (magenta)";
+            case 32: return "Soft Focus (low)";
+            case 33: return "Soft Focus";
+            case 34: return "Soft Focus (high)";
+            case 48: return "Miniature (auto)";
+            case 49: return "Miniature (top)";
+            case 50: return "Miniature (middle horizontal)";
+            case 51: return "Miniature (bottom)";
+            case 52: return "Miniature (left)";
+            case 53: return "Miniature (middle vertical)";
+            case 54: return "Miniature (right)";
+            case 64: return "HDR Painting (low)";
+            case 65: return "HDR Painting";
+            case 66: return "HDR Painting (high)";
+            case 80: return "Rich-tone Monochrome";
+            case 97: return "Water Color";
+            case 98: return "Water Color 2";
+            case 112: return "Illustration (low)";
+            case 113: return "Illustration";
+            case 114: return "Illustration (high)";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getSoftSkinEffectDescription()
+    {
+        return getIndexedDescription(TAG_SOFT_SKIN_EFFECT, "Off", "Low", "Mid", "High");
+    }
+
+    @Nullable
+    public String getVignettingCorrectionDescription()
+    {
+        return getIndexedDescription(TAG_VIGNETTING_CORRECTION, "Off", null, "Auto");
+    }
+
+    @Nullable
+    public String getLateralChromaticAberrationDescription()
+    {
+        return getIndexedDescription(TAG_LATERAL_CHROMATIC_ABERRATION, "Off", null, "Auto");
+    }
+
+    @Nullable
+    public String getDistortionCorrectionDescription()
+    {
+        return getIndexedDescription(TAG_DISTORTION_CORRECTION, "Off", null, "Auto");
+    }
+
+    @Nullable
+    public String getAutoPortraitFramedDescription()
+    {
+        return getIndexedDescription(TAG_AUTO_PORTRAIT_FRAMED, "No", "Yes");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        return getIndexedDescription(TAG_FOCUS_MODE,
+            "Manual", null, "AF-A", "AF-C", "AF-S", null, "DMF", "AF-D");
+    }
+
+    @Nullable
+    public String getAFPointSelectedDescription()
+    {
+        return getIndexedDescription(TAG_AF_POINT_SELECTED,
+            "Auto", // 0
+            "Center", // 1
+            "Top", // 2
+            "Upper-right", // 3
+            "Right", // 4
+            "Lower-right", // 5
+            "Bottom", // 6
+            "Lower-left", // 7
+            "Left", // 8
+            "Upper-left	  	", // 9
+            "Far Right", // 10
+            "Far Left", // 11
+            "Upper-middle", // 12
+            "Near Right", // 13
+            "Lower-middle", // 14
+            "Near Left", // 15
+            "Upper Far Right", // 16
+            "Lower Far Right", // 17
+            "Lower Far Left", // 18
+            "Upper Far Left" // 19
+        );
+    }
+
+    @Nullable
+    public String getSonyModelIdDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SONY_MODEL_ID);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 2: return "DSC-R1";
+            case 256: return "DSLR-A100";
+            case 257: return "DSLR-A900";
+            case 258: return "DSLR-A700";
+            case 259: return "DSLR-A200";
+            case 260: return "DSLR-A350";
+            case 261: return "DSLR-A300";
+            case 262: return "DSLR-A900 (APS-C mode)";
+            case 263: return "DSLR-A380/A390";
+            case 264: return "DSLR-A330";
+            case 265: return "DSLR-A230";
+            case 266: return "DSLR-A290";
+            case 269: return "DSLR-A850";
+            case 270: return "DSLR-A850 (APS-C mode)";
+            case 273: return "DSLR-A550";
+            case 274: return "DSLR-A500";
+            case 275: return "DSLR-A450";
+            case 278: return "NEX-5";
+            case 279: return "NEX-3";
+            case 280: return "SLT-A33";
+            case 281: return "SLT-A55V";
+            case 282: return "DSLR-A560";
+            case 283: return "DSLR-A580";
+            case 284: return "NEX-C3";
+            case 285: return "SLT-A35";
+            case 286: return "SLT-A65V";
+            case 287: return "SLT-A77V";
+            case 288: return "NEX-5N";
+            case 289: return "NEX-7";
+            case 290: return "NEX-VG20E";
+            case 291: return "SLT-A37";
+            case 292: return "SLT-A57";
+            case 293: return "NEX-F3";
+            case 294: return "SLT-A99V";
+            case 295: return "NEX-6";
+            case 296: return "NEX-5R";
+            case 297: return "DSC-RX100";
+            case 298: return "DSC-RX1";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_SCENE_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Standard";
+            case 1: return "Portrait";
+            case 2: return "Text";
+            case 3: return "Night Scene";
+            case 4: return "Sunset";
+            case 5: return "Sports";
+            case 6: return "Landscape";
+            case 7: return "Night Portrait";
+            case 8: return "Macro";
+            case 9: return "Super Macro";
+            case 16: return "Auto";
+            case 17: return "Night View/Portrait";
+            case 18: return "Sweep Panorama";
+            case 19: return "Handheld Night Shot";
+            case 20: return "Anti Motion Blur";
+            case 21: return "Cont. Priority AE";
+            case 22: return "Auto+";
+            case 23: return "3D Sweep Panorama";
+            case 24: return "Superior Auto";
+            case 25: return "High Sensitivity";
+            case 26: return "Fireworks";
+            case 27: return "Food";
+            case 28: return "Pet";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAFModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_AF_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Default";
+            case 1: return "Multi";
+            case 2: return "Center";
+            case 3: return "Spot";
+            case 4: return "Flexible Spot";
+            case 6: return "Touch";
+            case 14: return "Manual Focus";
+            case 15: return "Face Detected";
+            case 0xffff: return "n/a";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAFIlluminatorDescription()
+    {
+        Integer value = _directory.getInteger(TAG_AF_ILLUMINATOR);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Auto";
+            case 0xffff: return "n/a";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashLevelDescription()
+    {
+        Integer value = _directory.getInteger(TAG_FLASH_LEVEL);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case -32768: return "Low";
+            case -3: return "-3/3";
+            case -2: return "-2/3";
+            case -1: return "-1/3";
+            case 0: return "Normal";
+            case 1: return "+1/3";
+            case 2: return "+2/3";
+            case 3: return "+3/3";
+            case 128: return "n/a";
+            case 32767: return "High";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getReleaseModeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_RELEASE_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Normal";
+            case 2: return "Continuous";
+            case 5: return "Exposure Bracketing";
+            case 6: return "White Balance Bracketing";
+            case 65535: return "n/a";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSequenceNumberDescription()
+    {
+        Integer value = _directory.getInteger(TAG_RELEASE_MODE);
+
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0: return "Single";
+            case 65535: return "n/a";
+            default:
+                return value.toString();
+        }
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
new file mode 100644
index 0000000..ef5ce0d
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sony cameras that use the Sony Type 1 makernote tags.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SonyType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_CAMERA_INFO = 0x0010;
+    public static final int TAG_FOCUS_INFO = 0x0020;
+
+    public static final int TAG_IMAGE_QUALITY = 0x0102;
+    public static final int TAG_FLASH_EXPOSURE_COMP = 0x0104;
+    public static final int TAG_TELECONVERTER = 0x0105;
+
+    public static final int TAG_WHITE_BALANCE_FINE_TUNE = 0x0112;
+    public static final int TAG_CAMERA_SETTINGS = 0x0114;
+    public static final int TAG_WHITE_BALANCE = 0x0115;
+    public static final int TAG_EXTRA_INFO = 0x0116;
+
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+    public static final int TAG_MULTI_BURST_MODE = 0x1000;
+    public static final int TAG_MULTI_BURST_IMAGE_WIDTH = 0x1001;
+    public static final int TAG_MULTI_BURST_IMAGE_HEIGHT = 0x1002;
+    public static final int TAG_PANORAMA = 0x1003;
+
+    public static final int TAG_PREVIEW_IMAGE = 0x2001;
+    public static final int TAG_RATING = 0x2002;
+    public static final int TAG_CONTRAST = 0x2004;
+    public static final int TAG_SATURATION = 0x2005;
+    public static final int TAG_SHARPNESS = 0x2006;
+    public static final int TAG_BRIGHTNESS = 0x2007;
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION = 0x2008;
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION = 0x2009;
+    public static final int TAG_HDR = 0x200a;
+    public static final int TAG_MULTI_FRAME_NOISE_REDUCTION = 0x200b;
+    public static final int TAG_PICTURE_EFFECT = 0x200e;
+    public static final int TAG_SOFT_SKIN_EFFECT = 0x200f;
+
+    public static final int TAG_VIGNETTING_CORRECTION = 0x2011;
+    public static final int TAG_LATERAL_CHROMATIC_ABERRATION = 0x2012;
+    public static final int TAG_DISTORTION_CORRECTION = 0x2013;
+    public static final int TAG_WB_SHIFT_AMBER_MAGENTA = 0x2014;
+    public static final int TAG_AUTO_PORTRAIT_FRAMED = 0x2016;
+    public static final int TAG_FOCUS_MODE = 0x201b;
+    public static final int TAG_AF_POINT_SELECTED = 0x201e;
+
+    public static final int TAG_SHOT_INFO = 0x3000;
+
+    public static final int TAG_FILE_FORMAT = 0xb000;
+    public static final int TAG_SONY_MODEL_ID = 0xb001;
+
+    public static final int TAG_COLOR_MODE_SETTING = 0xb020;
+    public static final int TAG_COLOR_TEMPERATURE = 0xb021;
+    public static final int TAG_COLOR_COMPENSATION_FILTER = 0xb022;
+    public static final int TAG_SCENE_MODE = 0xb023;
+    public static final int TAG_ZONE_MATCHING = 0xb024;
+    public static final int TAG_DYNAMIC_RANGE_OPTIMISER = 0xb025;
+    public static final int TAG_IMAGE_STABILISATION = 0xb026;
+    public static final int TAG_LENS_ID = 0xb027;
+    public static final int TAG_MINOLTA_MAKERNOTE = 0xb028;
+    public static final int TAG_COLOR_MODE = 0xb029;
+    public static final int TAG_LENS_SPEC = 0xb02a;
+    public static final int TAG_FULL_IMAGE_SIZE = 0xb02b;
+    public static final int TAG_PREVIEW_IMAGE_SIZE = 0xb02c;
+
+    public static final int TAG_MACRO = 0xb040;
+    public static final int TAG_EXPOSURE_MODE = 0xb041;
+    public static final int TAG_FOCUS_MODE_2 = 0xb042;
+    public static final int TAG_AF_MODE = 0xb043;
+    public static final int TAG_AF_ILLUMINATOR = 0xb044;
+    public static final int TAG_JPEG_QUALITY = 0xb047;
+    public static final int TAG_FLASH_LEVEL = 0xb048;
+    public static final int TAG_RELEASE_MODE = 0xb049;
+    public static final int TAG_SEQUENCE_NUMBER = 0xb04a;
+    public static final int TAG_ANTI_BLUR = 0xb04b;
+    /**
+     * (FocusMode for RX100)
+     * 0 = Manual
+     * 2 = AF-S
+     * 3 = AF-C
+     * 5 = Semi-manual
+     * 6 = Direct Manual Focus
+     * (LongExposureNoiseReduction for other models)
+     * 0 = Off
+     * 1 = On
+     * 2 = On 2
+     * 65535 = n/a
+     */
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE = 0xb04e;
+    public static final int TAG_DYNAMIC_RANGE_OPTIMIZER = 0xb04f;
+
+    public static final int TAG_HIGH_ISO_NOISE_REDUCTION_2 = 0xb050;
+    public static final int TAG_INTELLIGENT_AUTO = 0xb052;
+    public static final int TAG_WHITE_BALANCE_2 = 0xb054;
+
+    public static final int TAG_NO_PRINT = 0xFFFF;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_CAMERA_INFO, "Camera Info");
+        _tagNameMap.put(TAG_FOCUS_INFO, "Focus Info");
+
+        _tagNameMap.put(TAG_IMAGE_QUALITY, "Image Quality");
+        _tagNameMap.put(TAG_FLASH_EXPOSURE_COMP, "Flash Exposure Compensation");
+        _tagNameMap.put(TAG_TELECONVERTER, "Teleconverter Model");
+
+        _tagNameMap.put(TAG_WHITE_BALANCE_FINE_TUNE, "White Balance Fine Tune Value");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_EXTRA_INFO, "Extra Info");
+
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching Info");
+
+        _tagNameMap.put(TAG_MULTI_BURST_MODE, "Multi Burst Mode");
+        _tagNameMap.put(TAG_MULTI_BURST_IMAGE_WIDTH, "Multi Burst Image Width");
+        _tagNameMap.put(TAG_MULTI_BURST_IMAGE_HEIGHT, "Multi Burst Image Height");
+        _tagNameMap.put(TAG_PANORAMA, "Panorama");
+
+        _tagNameMap.put(TAG_PREVIEW_IMAGE, "Preview Image");
+        _tagNameMap.put(TAG_RATING, "Rating");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_BRIGHTNESS, "Brightness");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_HDR, "HDR");
+        _tagNameMap.put(TAG_MULTI_FRAME_NOISE_REDUCTION, "Multi Frame Noise Reduction");
+        _tagNameMap.put(TAG_PICTURE_EFFECT, "Picture Effect");
+        _tagNameMap.put(TAG_SOFT_SKIN_EFFECT, "Soft Skin Effect");
+
+        _tagNameMap.put(TAG_VIGNETTING_CORRECTION, "Vignetting Correction");
+        _tagNameMap.put(TAG_LATERAL_CHROMATIC_ABERRATION, "Lateral Chromatic Aberration");
+        _tagNameMap.put(TAG_DISTORTION_CORRECTION, "Distortion Correction");
+        _tagNameMap.put(TAG_WB_SHIFT_AMBER_MAGENTA, "WB Shift Amber/Magenta");
+        _tagNameMap.put(TAG_AUTO_PORTRAIT_FRAMED, "Auto Portrait Framing");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_AF_POINT_SELECTED, "AF Point Selected");
+
+        _tagNameMap.put(TAG_SHOT_INFO, "Shot Info");
+
+        _tagNameMap.put(TAG_FILE_FORMAT, "File Format");
+        _tagNameMap.put(TAG_SONY_MODEL_ID, "Sony Model ID");
+
+        _tagNameMap.put(TAG_COLOR_MODE_SETTING, "Color Mode Setting");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_COLOR_COMPENSATION_FILTER, "Color Compensation Filter");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_ZONE_MATCHING, "Zone Matching");
+        _tagNameMap.put(TAG_DYNAMIC_RANGE_OPTIMISER, "Dynamic Range Optimizer");
+        _tagNameMap.put(TAG_IMAGE_STABILISATION, "Image Stabilisation");
+        _tagNameMap.put(TAG_LENS_ID, "Lens ID");
+        _tagNameMap.put(TAG_MINOLTA_MAKERNOTE, "Minolta Makernote");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_LENS_SPEC, "Lens Spec");
+        _tagNameMap.put(TAG_FULL_IMAGE_SIZE, "Full Image Size");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_SIZE, "Preview Image Size");
+
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(TAG_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(TAG_AF_MODE, "AF Mode");
+        _tagNameMap.put(TAG_AF_ILLUMINATOR, "AF Illuminator");
+        _tagNameMap.put(TAG_JPEG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FLASH_LEVEL, "Flash Level");
+        _tagNameMap.put(TAG_RELEASE_MODE, "Release Mode");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(TAG_ANTI_BLUR, "Anti Blur");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION_OR_FOCUS_MODE, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_DYNAMIC_RANGE_OPTIMIZER, "Dynamic Range Optimizer");
+
+        _tagNameMap.put(TAG_HIGH_ISO_NOISE_REDUCTION_2, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_INTELLIGENT_AUTO, "Intelligent Auto");
+        _tagNameMap.put(TAG_WHITE_BALANCE_2, "White Balance 2");
+
+        _tagNameMap.put(TAG_NO_PRINT, "No Print");
+    }
+
+    public SonyType1MakernoteDirectory()
+    {
+        this.setDescriptor(new SonyType1MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sony Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java b/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
new file mode 100644
index 0000000..8175dec
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SonyType6MakernoteDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SonyType6MakernoteDescriptor extends TagDescriptor<SonyType6MakernoteDirectory>
+{
+    public SonyType6MakernoteDescriptor(@NotNull SonyType6MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_THUMB_VERSION:
+                return getMakernoteThumbVersionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getMakernoteThumbVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_MAKERNOTE_THUMB_VERSION, 2);
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java b/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
new file mode 100644
index 0000000..a68d81e
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sony cameras that use the Sony Type 6 makernote tags.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class SonyType6MakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKERNOTE_THUMB_OFFSET = 0x0513;
+    public static final int TAG_MAKERNOTE_THUMB_LENGTH = 0x0514;
+//    public static final int TAG_UNKNOWN_1 = 0x0515;
+    public static final int TAG_MAKERNOTE_THUMB_VERSION = 0x2000;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_THUMB_OFFSET, "Makernote Thumb Offset");
+        _tagNameMap.put(TAG_MAKERNOTE_THUMB_LENGTH, "Makernote Thumb Length");
+//        _tagNameMap.put(TAG_UNKNOWN_1, "Sony-6-0x0203");
+        _tagNameMap.put(TAG_MAKERNOTE_THUMB_VERSION, "Makernote Thumb Version");
+    }
+
+    public SonyType6MakernoteDirectory()
+    {
+        this.setDescriptor(new SonyType6MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Sony Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/exif/makernotes/package.html b/src/com/drew/metadata/exif/makernotes/package.html
new file mode 100644
index 0000000..7115326
--- /dev/null
+++ b/src/com/drew/metadata/exif/makernotes/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the modelling of manufacturer-specific makernotes.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/exif/package.html b/src/com/drew/metadata/exif/package.html
new file mode 100644
index 0000000..0ec6f41
--- /dev/null
+++ b/src/com/drew/metadata/exif/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/file/FileMetadataDescriptor.java b/src/com/drew/metadata/file/FileMetadataDescriptor.java
new file mode 100644
index 0000000..31cd0de
--- /dev/null
+++ b/src/com/drew/metadata/file/FileMetadataDescriptor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FileMetadataDescriptor extends TagDescriptor<FileMetadataDirectory>
+{
+    public FileMetadataDescriptor(@NotNull FileMetadataDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case FileMetadataDirectory.TAG_FILE_SIZE:
+                return getFileSizeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getFileSizeDescription()
+    {
+        Long size = _directory.getLongObject(FileMetadataDirectory.TAG_FILE_SIZE);
+
+        if (size == null)
+            return null;
+
+        return Long.toString(size) + " bytes";
+    }
+}
+
diff --git a/src/com/drew/metadata/file/FileMetadataDirectory.java b/src/com/drew/metadata/file/FileMetadataDirectory.java
new file mode 100644
index 0000000..a2eff73
--- /dev/null
+++ b/src/com/drew/metadata/file/FileMetadataDirectory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+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;
+    }
+}
diff --git a/src/com/drew/metadata/file/FileMetadataReader.java b/src/com/drew/metadata/file/FileMetadataReader.java
new file mode 100644
index 0000000..5347c11
--- /dev/null
+++ b/src/com/drew/metadata/file/FileMetadataReader.java
@@ -0,0 +1,29 @@
+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);
+    }
+}
diff --git a/src/com/drew/metadata/file/package.html b/src/com/drew/metadata/file/package.html
new file mode 100644
index 0000000..8a3690d
--- /dev/null
+++ b/src/com/drew/metadata/file/package.html
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of file system metadata.
+
+<!-- Put @see and @since tags down here. -->
+ at since 2.8.0
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/iptc/IptcDescriptor.java b/src/com/drew/metadata/iptc/IptcDescriptor.java
new file mode 100644
index 0000000..49be662
--- /dev/null
+++ b/src/com/drew/metadata/iptc/IptcDescriptor.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.iptc;
+
+import com.drew.lang.StringUtil;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link IptcDirectory}.
+ * <p>
+ * As the IPTC directory already stores values as strings, this class simply returns the tag's value.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class IptcDescriptor extends TagDescriptor<IptcDirectory>
+{
+    public IptcDescriptor(@NotNull IptcDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case IptcDirectory.TAG_FILE_FORMAT:
+                return getFileFormatDescription();
+            case IptcDirectory.TAG_KEYWORDS:
+                return getKeywordsDescription();
+            case IptcDirectory.TAG_TIME_CREATED:
+                return getTimeCreatedDescription();
+            case IptcDirectory.TAG_DIGITAL_TIME_CREATED:
+                return getDigitalTimeCreatedDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFileFormatDescription()
+    {
+        Integer value = _directory.getInteger(IptcDirectory.TAG_FILE_FORMAT);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "No ObjectData";
+            case 1: return "IPTC-NAA Digital Newsphoto Parameter Record";
+            case 2: return "IPTC7901 Recommended Message Format";
+            case 3: return "Tagged Image File Format (Adobe/Aldus Image data)";
+            case 4: return "Illustrator (Adobe Graphics data)";
+            case 5: return "AppleSingle (Apple Computer Inc)";
+            case 6: return "NAA 89-3 (ANPA 1312)";
+            case 7: return "MacBinary II";
+            case 8: return "IPTC Unstructured Character Oriented File Format (UCOFF)";
+            case 9: return "United Press International ANPA 1312 variant";
+            case 10: return "United Press International Down-Load Message";
+            case 11: return "JPEG File Interchange (JFIF)";
+            case 12: return "Photo-CD Image-Pac (Eastman Kodak)";
+            case 13: return "Bit Mapped Graphics File [.BMP] (Microsoft)";
+            case 14: return "Digital Audio File [.WAV] (Microsoft & Creative Labs)";
+            case 15: return "Audio plus Moving Video [.AVI] (Microsoft)";
+            case 16: return "PC DOS/Windows Executable Files [.COM][.EXE]";
+            case 17: return "Compressed Binary File [.ZIP] (PKWare Inc)";
+            case 18: return "Audio Interchange File Format AIFF (Apple Computer Inc)";
+            case 19: return "RIFF Wave (Microsoft Corporation)";
+            case 20: return "Freehand (Macromedia/Aldus)";
+            case 21: return "Hypertext Markup Language [.HTML] (The Internet Society)";
+            case 22: return "MPEG 2 Audio Layer 2 (Musicom), ISO/IEC";
+            case 23: return "MPEG 2 Audio Layer 3, ISO/IEC";
+            case 24: return "Portable Document File [.PDF] Adobe";
+            case 25: return "News Industry Text Format (NITF)";
+            case 26: return "Tape Archive [.TAR]";
+            case 27: return "Tidningarnas Telegrambyra NITF version (TTNITF DTD)";
+            case 28: return "Ritzaus Bureau NITF version (RBNITF DTD)";
+            case 29: return "Corel Draw [.CDR]";
+        }
+        return String.format("Unknown (%d)", value);
+    }
+
+    @Nullable
+    public String getByLineDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_BY_LINE);
+    }
+
+    @Nullable
+    public String getByLineTitleDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_BY_LINE_TITLE);
+    }
+
+    @Nullable
+    public String getCaptionDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CAPTION);
+    }
+
+    @Nullable
+    public String getCategoryDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CATEGORY);
+    }
+
+    @Nullable
+    public String getCityDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CITY);
+    }
+
+    @Nullable
+    public String getCopyrightNoticeDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_COPYRIGHT_NOTICE);
+    }
+
+    @Nullable
+    public String getCountryOrPrimaryLocationDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME);
+    }
+
+    @Nullable
+    public String getCreditDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CREDIT);
+    }
+
+    @Nullable
+    public String getDateCreatedDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_DATE_CREATED);
+    }
+
+    @Nullable
+    public String getHeadlineDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_HEADLINE);
+    }
+
+    @Nullable
+    public String getKeywordsDescription()
+    {
+        final String[] keywords = _directory.getStringArray(IptcDirectory.TAG_KEYWORDS);
+        if (keywords==null)
+            return null;
+        return StringUtil.join(keywords, ";");
+    }
+
+    @Nullable
+    public String getObjectNameDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_OBJECT_NAME);
+    }
+
+    @Nullable
+    public String getOriginalTransmissionReferenceDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE);
+    }
+
+    @Nullable
+    public String getOriginatingProgramDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_ORIGINATING_PROGRAM);
+    }
+
+    @Nullable
+    public String getProvinceOrStateDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_PROVINCE_OR_STATE);
+    }
+
+    @Nullable
+    public String getRecordVersionDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_APPLICATION_RECORD_VERSION);
+    }
+
+    @Nullable
+    public String getReleaseDateDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_RELEASE_DATE);
+    }
+
+    @Nullable
+    public String getReleaseTimeDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_RELEASE_TIME);
+    }
+
+    @Nullable
+    public String getSourceDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_SOURCE);
+    }
+
+    @Nullable
+    public String getSpecialInstructionsDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS);
+    }
+
+    @Nullable
+    public String getSupplementalCategoriesDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES);
+    }
+
+    @Nullable
+    public String getTimeCreatedDescription()
+    {
+        String s = _directory.getString(IptcDirectory.TAG_TIME_CREATED);
+        if (s == null)
+            return null;
+        if (s.length() == 6 || s.length() == 11)
+            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
+        return s;
+    }
+
+    @Nullable
+    public String getDigitalTimeCreatedDescription()
+    {
+        String s = _directory.getString(IptcDirectory.TAG_DIGITAL_TIME_CREATED);
+        if (s == null)
+            return null;
+        if (s.length() == 6 || s.length() == 11)
+            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
+        return s;
+    }
+
+    @Nullable
+    public String getUrgencyDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_URGENCY);
+    }
+
+    @Nullable
+    public String getWriterDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CAPTION_WRITER);
+    }
+}
diff --git a/src/com/drew/metadata/iptc/IptcDirectory.java b/src/com/drew/metadata/iptc/IptcDirectory.java
new file mode 100644
index 0000000..b864a9b
--- /dev/null
+++ b/src/com/drew/metadata/iptc/IptcDirectory.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.iptc;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Describes tags used by the International Press Telecommunications Council (IPTC) metadata format.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class IptcDirectory extends Directory
+{
+    // IPTC EnvelopeRecord Tags
+    public static final int TAG_ENVELOPE_RECORD_VERSION          = 0x0100; // 0 + 0x0100
+    public static final int TAG_DESTINATION                      = 0x0105; // 5
+    public static final int TAG_FILE_FORMAT                      = 0x0114; // 20
+    public static final int TAG_FILE_VERSION                     = 0x0116; // 22
+    public static final int TAG_SERVICE_ID                       = 0x011E; // 30
+    public static final int TAG_ENVELOPE_NUMBER                  = 0x0128; // 40
+    public static final int TAG_PRODUCT_ID                       = 0x0132; // 50
+    public static final int TAG_ENVELOPE_PRIORITY                = 0x013C; // 60
+    public static final int TAG_DATE_SENT                        = 0x0146; // 70
+    public static final int TAG_TIME_SENT                        = 0x0150; // 80
+    public static final int TAG_CODED_CHARACTER_SET              = 0x015A; // 90
+    public static final int TAG_UNIQUE_OBJECT_NAME               = 0x0164; // 100
+    public static final int TAG_ARM_IDENTIFIER                   = 0x0178; // 120
+    public static final int TAG_ARM_VERSION                      = 0x017a; // 122
+
+    // IPTC ApplicationRecord Tags
+    public static final int TAG_APPLICATION_RECORD_VERSION       = 0x0200; // 0 + 0x0200
+    public static final int TAG_OBJECT_TYPE_REFERENCE            = 0x0203; // 3
+    public static final int TAG_OBJECT_ATTRIBUTE_REFERENCE       = 0x0204; // 4
+    public static final int TAG_OBJECT_NAME                      = 0x0205; // 5
+    public static final int TAG_EDIT_STATUS                      = 0x0207; // 7
+    public static final int TAG_EDITORIAL_UPDATE                 = 0x0208; // 8
+    public static final int TAG_URGENCY                          = 0X020A; // 10
+    public static final int TAG_SUBJECT_REFERENCE                = 0X020C; // 12
+    public static final int TAG_CATEGORY                         = 0x020F; // 15
+    public static final int TAG_SUPPLEMENTAL_CATEGORIES          = 0x0214; // 20
+    public static final int TAG_FIXTURE_ID                       = 0x0216; // 22
+    public static final int TAG_KEYWORDS                         = 0x0219; // 25
+    public static final int TAG_CONTENT_LOCATION_CODE            = 0x021A; // 26
+    public static final int TAG_CONTENT_LOCATION_NAME            = 0x021B; // 27
+    public static final int TAG_RELEASE_DATE                     = 0X021E; // 30
+    public static final int TAG_RELEASE_TIME                     = 0x0223; // 35
+    public static final int TAG_EXPIRATION_DATE                  = 0x0225; // 37
+    public static final int TAG_EXPIRATION_TIME                  = 0x0226; // 38
+    public static final int TAG_SPECIAL_INSTRUCTIONS             = 0x0228; // 40
+    public static final int TAG_ACTION_ADVISED                   = 0x022A; // 42
+    public static final int TAG_REFERENCE_SERVICE                = 0x022D; // 45
+    public static final int TAG_REFERENCE_DATE                   = 0x022F; // 47
+    public static final int TAG_REFERENCE_NUMBER                 = 0x0232; // 50
+    public static final int TAG_DATE_CREATED                     = 0x0237; // 55
+    public static final int TAG_TIME_CREATED                     = 0X023C; // 60
+    public static final int TAG_DIGITAL_DATE_CREATED             = 0x023E; // 62
+    public static final int TAG_DIGITAL_TIME_CREATED             = 0x023F; // 63
+    public static final int TAG_ORIGINATING_PROGRAM              = 0x0241; // 65
+    public static final int TAG_PROGRAM_VERSION                  = 0x0246; // 70
+    public static final int TAG_OBJECT_CYCLE                     = 0x024B; // 75
+    public static final int TAG_BY_LINE                          = 0x0250; // 80
+    public static final int TAG_BY_LINE_TITLE                    = 0x0255; // 85
+    public static final int TAG_CITY                             = 0x025A; // 90
+    public static final int TAG_SUB_LOCATION                     = 0x025C; // 92
+    public static final int TAG_PROVINCE_OR_STATE                = 0x025F; // 95
+    public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE = 0x0264; // 100
+    public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME = 0x0265; // 101
+    public static final int TAG_ORIGINAL_TRANSMISSION_REFERENCE  = 0x0267; // 103
+    public static final int TAG_HEADLINE                         = 0x0269; // 105
+    public static final int TAG_CREDIT                           = 0x026E; // 110
+    public static final int TAG_SOURCE                           = 0x0273; // 115
+    public static final int TAG_COPYRIGHT_NOTICE                 = 0x0274; // 116
+    public static final int TAG_CONTACT                          = 0x0276; // 118
+    public static final int TAG_CAPTION                          = 0x0278; // 120
+    public static final int TAG_LOCAL_CAPTION                    = 0x0279; // 121
+    public static final int TAG_CAPTION_WRITER                   = 0x027A; // 122
+    public static final int TAG_RASTERIZED_CAPTION               = 0x027D; // 125
+    public static final int TAG_IMAGE_TYPE                       = 0x0282; // 130
+    public static final int TAG_IMAGE_ORIENTATION                = 0x0283; // 131
+    public static final int TAG_LANGUAGE_IDENTIFIER              = 0x0287; // 135
+    public static final int TAG_AUDIO_TYPE                       = 0x0296; // 150
+    public static final int TAG_AUDIO_SAMPLING_RATE              = 0x0297; // 151
+    public static final int TAG_AUDIO_SAMPLING_RESOLUTION        = 0x0298; // 152
+    public static final int TAG_AUDIO_DURATION                   = 0x0299; // 153
+    public static final int TAG_AUDIO_OUTCUE                     = 0x029A; // 154
+
+    public static final int TAG_JOB_ID                           = 0x02B8; // 184
+    public static final int TAG_MASTER_DOCUMENT_ID               = 0x02B9; // 185
+    public static final int TAG_SHORT_DOCUMENT_ID                = 0x02BA; // 186
+    public static final int TAG_UNIQUE_DOCUMENT_ID               = 0x02BB; // 187
+    public static final int TAG_OWNER_ID                         = 0x02BC; // 188
+
+    public static final int TAG_OBJECT_PREVIEW_FILE_FORMAT       = 0x02C8; // 200
+    public static final int TAG_OBJECT_PREVIEW_FILE_FORMAT_VERSION  = 0x02C9; // 201
+    public static final int TAG_OBJECT_PREVIEW_DATA              = 0x02CA; // 202
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_ENVELOPE_RECORD_VERSION, "Enveloped Record Version");
+        _tagNameMap.put(TAG_DESTINATION, "Destination");
+        _tagNameMap.put(TAG_FILE_FORMAT, "File Format");
+        _tagNameMap.put(TAG_FILE_VERSION, "File Version");
+        _tagNameMap.put(TAG_SERVICE_ID, "Service Identifier");
+        _tagNameMap.put(TAG_ENVELOPE_NUMBER, "Envelope Number");
+        _tagNameMap.put(TAG_PRODUCT_ID, "Product Identifier");
+        _tagNameMap.put(TAG_ENVELOPE_PRIORITY, "Envelope Priority");
+        _tagNameMap.put(TAG_DATE_SENT, "Date Sent");
+        _tagNameMap.put(TAG_TIME_SENT, "Time Sent");
+        _tagNameMap.put(TAG_CODED_CHARACTER_SET, "Coded Character Set");
+        _tagNameMap.put(TAG_UNIQUE_OBJECT_NAME, "Unique Object Name");
+        _tagNameMap.put(TAG_ARM_IDENTIFIER, "ARM Identifier");
+        _tagNameMap.put(TAG_ARM_VERSION, "ARM Version");
+
+        _tagNameMap.put(TAG_APPLICATION_RECORD_VERSION, "Application Record Version");
+        _tagNameMap.put(TAG_OBJECT_TYPE_REFERENCE, "Object Type Reference");
+        _tagNameMap.put(TAG_OBJECT_ATTRIBUTE_REFERENCE, "Object Attribute Reference");
+        _tagNameMap.put(TAG_OBJECT_NAME, "Object Name");
+        _tagNameMap.put(TAG_EDIT_STATUS, "Edit Status");
+        _tagNameMap.put(TAG_EDITORIAL_UPDATE, "Editorial Update");
+        _tagNameMap.put(TAG_URGENCY, "Urgency");
+        _tagNameMap.put(TAG_SUBJECT_REFERENCE, "Subject Reference");
+        _tagNameMap.put(TAG_CATEGORY, "Category");
+        _tagNameMap.put(TAG_SUPPLEMENTAL_CATEGORIES, "Supplemental Category(s)");
+        _tagNameMap.put(TAG_FIXTURE_ID, "Fixture Identifier");
+        _tagNameMap.put(TAG_KEYWORDS, "Keywords");
+        _tagNameMap.put(TAG_CONTENT_LOCATION_CODE, "Content Location Code");
+        _tagNameMap.put(TAG_CONTENT_LOCATION_NAME, "Content Location Name");
+        _tagNameMap.put(TAG_RELEASE_DATE, "Release Date");
+        _tagNameMap.put(TAG_RELEASE_TIME, "Release Time");
+        _tagNameMap.put(TAG_EXPIRATION_DATE, "Expiration Date");
+        _tagNameMap.put(TAG_EXPIRATION_TIME, "Expiration Time");
+        _tagNameMap.put(TAG_SPECIAL_INSTRUCTIONS, "Special Instructions");
+        _tagNameMap.put(TAG_ACTION_ADVISED, "Action Advised");
+        _tagNameMap.put(TAG_REFERENCE_SERVICE, "Reference Service");
+        _tagNameMap.put(TAG_REFERENCE_DATE, "Reference Date");
+        _tagNameMap.put(TAG_REFERENCE_NUMBER, "Reference Number");
+        _tagNameMap.put(TAG_DATE_CREATED, "Date Created");
+        _tagNameMap.put(TAG_TIME_CREATED, "Time Created");
+        _tagNameMap.put(TAG_DIGITAL_DATE_CREATED, "Digital Date Created");
+        _tagNameMap.put(TAG_DIGITAL_TIME_CREATED, "Digital Time Created");
+        _tagNameMap.put(TAG_ORIGINATING_PROGRAM, "Originating Program");
+        _tagNameMap.put(TAG_PROGRAM_VERSION, "Program Version");
+        _tagNameMap.put(TAG_OBJECT_CYCLE, "Object Cycle");
+        _tagNameMap.put(TAG_BY_LINE, "By-line");
+        _tagNameMap.put(TAG_BY_LINE_TITLE, "By-line Title");
+        _tagNameMap.put(TAG_CITY, "City");
+        _tagNameMap.put(TAG_SUB_LOCATION, "Sub-location");
+        _tagNameMap.put(TAG_PROVINCE_OR_STATE, "Province/State");
+        _tagNameMap.put(TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE, "Country/Primary Location Code");
+        _tagNameMap.put(TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME, "Country/Primary Location Name");
+        _tagNameMap.put(TAG_ORIGINAL_TRANSMISSION_REFERENCE, "Original Transmission Reference");
+        _tagNameMap.put(TAG_HEADLINE, "Headline");
+        _tagNameMap.put(TAG_CREDIT, "Credit");
+        _tagNameMap.put(TAG_SOURCE, "Source");
+        _tagNameMap.put(TAG_COPYRIGHT_NOTICE, "Copyright Notice");
+        _tagNameMap.put(TAG_CONTACT, "Contact");
+        _tagNameMap.put(TAG_CAPTION, "Caption/Abstract");
+        _tagNameMap.put(TAG_LOCAL_CAPTION, "Local Caption");
+        _tagNameMap.put(TAG_CAPTION_WRITER, "Caption Writer/Editor");
+        _tagNameMap.put(TAG_RASTERIZED_CAPTION, "Rasterized Caption");
+        _tagNameMap.put(TAG_IMAGE_TYPE, "Image Type");
+        _tagNameMap.put(TAG_IMAGE_ORIENTATION, "Image Orientation");
+        _tagNameMap.put(TAG_LANGUAGE_IDENTIFIER, "Language Identifier");
+        _tagNameMap.put(TAG_AUDIO_TYPE, "Audio Type");
+        _tagNameMap.put(TAG_AUDIO_SAMPLING_RATE, "Audio Sampling Rate");
+        _tagNameMap.put(TAG_AUDIO_SAMPLING_RESOLUTION, "Audio Sampling Resolution");
+        _tagNameMap.put(TAG_AUDIO_DURATION, "Audio Duration");
+        _tagNameMap.put(TAG_AUDIO_OUTCUE, "Audio Outcue");
+
+        _tagNameMap.put(TAG_JOB_ID, "Job Identifier");
+        _tagNameMap.put(TAG_MASTER_DOCUMENT_ID, "Master Document Identifier");
+        _tagNameMap.put(TAG_SHORT_DOCUMENT_ID, "Short Document Identifier");
+        _tagNameMap.put(TAG_UNIQUE_DOCUMENT_ID, "Unique Document Identifier");
+        _tagNameMap.put(TAG_OWNER_ID, "Owner Identifier");
+
+        _tagNameMap.put(TAG_OBJECT_PREVIEW_FILE_FORMAT, "Object Data Preview File Format");
+        _tagNameMap.put(TAG_OBJECT_PREVIEW_FILE_FORMAT_VERSION, "Object Data Preview File Format Version");
+        _tagNameMap.put(TAG_OBJECT_PREVIEW_DATA, "Object Data Preview Data");
+    }
+
+    public IptcDirectory()
+    {
+        this.setDescriptor(new IptcDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "IPTC";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * Returns any keywords contained in the IPTC data.  This value may be <code>null</code>.
+     */
+    @Nullable
+    public List<String> getKeywords()
+    {
+        final String[] array = getStringArray(IptcDirectory.TAG_KEYWORDS);
+        if (array==null)
+            return null;
+        return Arrays.asList(array);
+    }
+}
diff --git a/src/com/drew/metadata/iptc/IptcReader.java b/src/com/drew/metadata/iptc/IptcReader.java
new file mode 100644
index 0000000..cf5ec7f
--- /dev/null
+++ b/src/com/drew/metadata/iptc/IptcReader.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.iptc;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * Decodes IPTC binary data, populating a {@link Metadata} object with tag values in an {@link IptcDirectory}.
+ * <p>
+ * http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class IptcReader implements JpegSegmentMetadataReader
+{
+    // TODO consider breaking the IPTC section up into multiple directories and providing segregation of each IPTC directory
+/*
+    public static final int DIRECTORY_IPTC = 2;
+
+    public static final int ENVELOPE_RECORD = 1;
+    public static final int APPLICATION_RECORD_2 = 2;
+    public static final int APPLICATION_RECORD_3 = 3;
+    public static final int APPLICATION_RECORD_4 = 4;
+    public static final int APPLICATION_RECORD_5 = 5;
+    public static final int APPLICATION_RECORD_6 = 6;
+    public static final int PRE_DATA_RECORD = 7;
+    public static final int DATA_RECORD = 8;
+    public static final int POST_DATA_RECORD = 9;
+*/
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Arrays.asList(JpegSegmentType.APPD);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the IPTC marker byte
+            if (segmentBytes.length != 0 && segmentBytes[0] == 0x1c) {
+                extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
+            }
+        }
+    }
+
+    /**
+     * Performs the IPTC data extraction, adding found values to the specified instance of {@link Metadata}.
+     */
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length)
+    {
+        IptcDirectory directory = new IptcDirectory();
+        metadata.addDirectory(directory);
+
+        int offset = 0;
+
+        // for each tag
+        while (offset < length) {
+
+            // identifies start of a tag
+            short startByte;
+            try {
+                startByte = reader.getUInt8();
+                offset++;
+            } catch (IOException e) {
+                directory.addError("Unable to read starting byte of IPTC tag");
+                return;
+            }
+
+            if (startByte != 0x1c) {
+                // NOTE have seen images where there was one extra byte at the end, giving
+                // offset==length at this point, which is not worth logging as an error.
+                if (offset != length)
+                    directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x1c' but got '0x" + Integer.toHexString(startByte) + "'.");
+                return;
+            }
+
+            // we need at least five bytes left to read a tag
+            if (offset + 5 >= length) {
+                directory.addError("Too few bytes remain for a valid IPTC tag");
+                return;
+            }
+
+            int directoryType;
+            int tagType;
+            int tagByteCount;
+            try {
+                directoryType = reader.getUInt8();
+                tagType = reader.getUInt8();
+                // TODO support Extended DataSet Tag (see 1.5(c), p14, IPTC-IIMV4.2.pdf)
+                tagByteCount = reader.getUInt16();
+                offset += 4;
+            } catch (IOException e) {
+                directory.addError("IPTC data segment ended mid-way through tag descriptor");
+                return;
+            }
+
+            if (offset + tagByteCount > length) {
+                directory.addError("Data for tag extends beyond end of IPTC segment");
+                return;
+            }
+
+            try {
+                processTag(reader, directory, directoryType, tagType, tagByteCount);
+            } catch (IOException e) {
+                directory.addError("Error processing IPTC tag");
+                return;
+            }
+
+            offset += tagByteCount;
+        }
+    }
+
+    private void processTag(@NotNull SequentialReader reader, @NotNull Directory directory, int directoryType, int tagType, int tagByteCount) throws IOException
+    {
+        int tagIdentifier = tagType | (directoryType << 8);
+
+        // Some images have been seen that specify a zero byte tag, which cannot be of much use.
+        // We elect here to completely ignore the tag. The IPTC specification doesn't mention
+        // anything about the interpretation of this situation.
+        // https://raw.githubusercontent.com/wiki/drewnoakes/metadata-extractor/docs/IPTC-IIMV4.2.pdf
+        if (tagByteCount == 0) {
+            directory.setString(tagIdentifier, "");
+            return;
+        }
+
+        String string = null;
+
+        switch (tagIdentifier) {
+            case IptcDirectory.TAG_CODED_CHARACTER_SET:
+                byte[] bytes = reader.getBytes(tagByteCount);
+                String charset = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes);
+                if (charset == null) {
+                    // Unable to determine the charset, so fall through and treat tag as a regular string
+                    string = new String(bytes);
+                    break;
+                }
+                directory.setString(tagIdentifier, charset);
+                return;
+            case IptcDirectory.TAG_ENVELOPE_RECORD_VERSION:
+            case IptcDirectory.TAG_APPLICATION_RECORD_VERSION:
+            case IptcDirectory.TAG_FILE_VERSION:
+            case IptcDirectory.TAG_ARM_VERSION:
+            case IptcDirectory.TAG_PROGRAM_VERSION:
+                // short
+                if (tagByteCount >= 2) {
+                    int shortValue = reader.getUInt16();
+                    reader.skip(tagByteCount - 2);
+                    directory.setInt(tagIdentifier, shortValue);
+                    return;
+                }
+                break;
+            case IptcDirectory.TAG_URGENCY:
+                // byte
+                directory.setInt(tagIdentifier, reader.getUInt8());
+                reader.skip(tagByteCount - 1);
+                return;
+            case IptcDirectory.TAG_RELEASE_DATE:
+            case IptcDirectory.TAG_DATE_CREATED:
+                // Date object
+                if (tagByteCount >= 8) {
+                    string = reader.getString(tagByteCount);
+                    try {
+                        int year = Integer.parseInt(string.substring(0, 4));
+                        int month = Integer.parseInt(string.substring(4, 6)) - 1;
+                        int day = Integer.parseInt(string.substring(6, 8));
+                        Date date = new java.util.GregorianCalendar(year, month, day).getTime();
+                        directory.setDate(tagIdentifier, date);
+                        return;
+                    } catch (NumberFormatException e) {
+                        // fall through and we'll process the 'string' value below
+                    }
+                } else {
+                    reader.skip(tagByteCount);
+                }
+            case IptcDirectory.TAG_RELEASE_TIME:
+            case IptcDirectory.TAG_TIME_CREATED:
+                // time...
+            default:
+                // fall through
+        }
+
+        // If we haven't returned yet, treat it as a string
+        // NOTE that there's a chance we've already loaded the value as a string above, but failed to parse the value
+        if (string == null) {
+            String encoding = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET);
+            if (encoding != null) {
+                string = reader.getString(tagByteCount, encoding);
+            } else {
+                byte[] bytes = reader.getBytes(tagByteCount);
+                encoding = Iso2022Converter.guessEncoding(bytes);
+                string = encoding != null ? new String(bytes, encoding) : new String(bytes);
+            }
+        }
+
+        if (directory.containsTag(tagIdentifier)) {
+            // this fancy string[] business avoids using an ArrayList for performance reasons
+            String[] oldStrings = directory.getStringArray(tagIdentifier);
+            String[] newStrings;
+            if (oldStrings == null) {
+                newStrings = new String[1];
+            } else {
+                newStrings = new String[oldStrings.length + 1];
+                System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length);
+            }
+            newStrings[newStrings.length - 1] = string;
+            directory.setStringArray(tagIdentifier, newStrings);
+        } else {
+            directory.setString(tagIdentifier, string);
+        }
+    }
+}
diff --git a/src/com/drew/metadata/iptc/Iso2022Converter.java b/src/com/drew/metadata/iptc/Iso2022Converter.java
new file mode 100644
index 0000000..5edd749
--- /dev/null
+++ b/src/com/drew/metadata/iptc/Iso2022Converter.java
@@ -0,0 +1,83 @@
+package com.drew.metadata.iptc;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+
+public final class Iso2022Converter
+{
+    private static final String ISO_8859_1 = "ISO-8859-1";
+    private static final String UTF_8 = "UTF-8";
+
+    private static final byte LATIN_CAPITAL_A = 0x41;
+    private static final int DOT = 0xe280a2;
+    private static final byte LATIN_CAPITAL_G = 0x47;
+    private static final byte PERCENT_SIGN = 0x25;
+    private static final byte ESC = 0x1B;
+
+    /**
+     * Converts the given ISO2022 char set to a Java charset name.
+     *
+     * @param bytes string data encoded using ISO2022
+     * @return the Java charset name as a string, or <code>null</code> if the conversion was not possible
+     */
+    @Nullable
+    public static String convertISO2022CharsetToJavaCharset(@NotNull final byte[] bytes)
+    {
+        if (bytes.length > 2 && bytes[0] == ESC && bytes[1] == PERCENT_SIGN && bytes[2] == LATIN_CAPITAL_G)
+            return UTF_8;
+
+        if (bytes.length > 3 && bytes[0] == ESC && (bytes[3] & 0xFF | ((bytes[2] & 0xFF) << 8) | ((bytes[1] & 0xFF) << 16)) == DOT && bytes[4] == LATIN_CAPITAL_A)
+            return ISO_8859_1;
+
+        return null;
+    }
+
+    /**
+     * Attempts to guess the encoding of a string provided as a byte array.
+     * <p/>
+     * Encodings trialled are, in order:
+     * <ul>
+     *     <li>UTF-8</li>
+     *     <li><code>System.getProperty("file.encoding")</code></li>
+     *     <li>ISO-8859-1</li>
+     * </ul>
+     * <p/>
+     * Its only purpose is to guess the encoding if and only if iptc tag coded character set is not set. If the
+     * encoding is not UTF-8, the tag should be set. Otherwise it is bad practice. This method tries to
+     * workaround this issue since some metadata manipulating tools do not prevent such bad practice.
+     * <p/>
+     * About the reliability of this method: The check if some bytes are UTF-8 or not has a very high reliability.
+     * The two other checks are less reliable.
+     *
+     * @param bytes some text as bytes
+     * @return the name of the encoding or null if none could be guessed
+     */
+    @Nullable
+    static String guessEncoding(@NotNull final byte[] bytes)
+    {
+        String[] encodings = { UTF_8, System.getProperty("file.encoding"), ISO_8859_1 };
+
+        for (String encoding : encodings)
+        {
+            CharsetDecoder cs = Charset.forName(encoding).newDecoder();
+
+            try {
+                cs.decode(ByteBuffer.wrap(bytes));
+                return encoding;
+            } catch (CharacterCodingException e) {
+                // fall through...
+            }
+        }
+
+        // No encodings succeeded. Return null.
+        return null;
+    }
+
+    private Iso2022Converter()
+    {}
+}
diff --git a/src/com/drew/metadata/iptc/package.html b/src/com/drew/metadata/iptc/package.html
new file mode 100644
index 0000000..40c60b3
--- /dev/null
+++ b/src/com/drew/metadata/iptc/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of IPTC metadata.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java b/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java
new file mode 100644
index 0000000..c5a67f8
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link JpegCommentDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegCommentDescriptor extends TagDescriptor<JpegCommentDirectory>
+{
+    public JpegCommentDescriptor(@NotNull JpegCommentDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Nullable
+    public String getJpegCommentDescription()
+    {
+        return _directory.getString(JpegCommentDirectory.TAG_COMMENT);
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/JpegCommentDirectory.java b/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
new file mode 100644
index 0000000..7e077fa
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags used by a JPEG file comment.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegCommentDirectory extends Directory
+{
+    /**
+     * This value does not apply to a particular standard. Rather, this value has been fabricated to maintain
+     * consistency with other directory types.
+     */
+    public static final int TAG_COMMENT = 0;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_COMMENT, "JPEG Comment");
+    }
+
+    public JpegCommentDirectory()
+    {
+        this.setDescriptor(new JpegCommentDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "JpegComment";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/JpegCommentReader.java b/src/com/drew/metadata/jpeg/JpegCommentReader.java
new file mode 100644
index 0000000..bcc6cfb
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegCommentReader.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.util.Arrays;
+
+/**
+ * Decodes the comment stored within JPEG files, populating a {@link Metadata} object with tag values in a
+ * {@link JpegCommentDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegCommentReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Arrays.asList(JpegSegmentType.COM);
+    }
+
+    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    {
+        // The entire contents of the byte[] is the comment. There's nothing here to discriminate upon.
+        return true;
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            JpegCommentDirectory directory = new JpegCommentDirectory();
+            metadata.addDirectory(directory);
+
+            // The entire contents of the directory are the comment
+            directory.setString(JpegCommentDirectory.TAG_COMMENT, new String(segmentBytes));
+        }
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/JpegComponent.java b/src/com/drew/metadata/jpeg/JpegComponent.java
new file mode 100644
index 0000000..06558ec
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegComponent.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.lang.annotations.Nullable;
+
+import java.io.Serializable;
+
+/**
+ * Stores information about a JPEG image component such as the component id, horiz/vert sampling factor and
+ * quantization table number.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegComponent implements Serializable
+{
+    private static final long serialVersionUID = 61121257899091914L;
+
+    private final int _componentId;
+    private final int _samplingFactorByte;
+    private final int _quantizationTableNumber;
+
+    public JpegComponent(int componentId, int samplingFactorByte, int quantizationTableNumber)
+    {
+        _componentId = componentId;
+        _samplingFactorByte = samplingFactorByte;
+        _quantizationTableNumber = quantizationTableNumber;
+    }
+
+    public int getComponentId()
+    {
+        return _componentId;
+    }
+
+    /**
+     * Returns the component name (one of: Y, Cb, Cr, I, or Q)
+     * @return the component name
+     */
+    @Nullable
+    public String getComponentName()
+    {
+        switch (_componentId)
+        {
+            case 1:
+                return "Y";
+            case 2:
+                return "Cb";
+            case 3:
+                return "Cr";
+            case 4:
+                return "I";
+            case 5:
+                return "Q";
+        }
+        return null;
+    }
+
+    public int getQuantizationTableNumber()
+    {
+        return _quantizationTableNumber;
+    }
+
+    public int getHorizontalSamplingFactor()
+    {
+        return _samplingFactorByte & 0x0F;
+    }
+
+    public int getVerticalSamplingFactor()
+    {
+        return (_samplingFactorByte>>4) & 0x0F;
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/JpegDescriptor.java b/src/com/drew/metadata/jpeg/JpegDescriptor.java
new file mode 100644
index 0000000..3989d20
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegDescriptor.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string versions of the tags stored in a JpegDirectory.
+ * Thanks to Darrell Silver (www.darrellsilver.com) for the initial version of this class.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class JpegDescriptor extends TagDescriptor<JpegDirectory>
+{
+    public JpegDescriptor(@NotNull JpegDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType)
+        {
+            case JpegDirectory.TAG_COMPRESSION_TYPE:
+                return getImageCompressionTypeDescription();
+            case JpegDirectory.TAG_COMPONENT_DATA_1:
+                return getComponentDataDescription(0);
+            case JpegDirectory.TAG_COMPONENT_DATA_2:
+                return getComponentDataDescription(1);
+            case JpegDirectory.TAG_COMPONENT_DATA_3:
+                return getComponentDataDescription(2);
+            case JpegDirectory.TAG_COMPONENT_DATA_4:
+                return getComponentDataDescription(3);
+            case JpegDirectory.TAG_DATA_PRECISION:
+                return getDataPrecisionDescription();
+            case JpegDirectory.TAG_IMAGE_HEIGHT:
+                return getImageHeightDescription();
+            case JpegDirectory.TAG_IMAGE_WIDTH:
+                return getImageWidthDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getImageCompressionTypeDescription()
+    {
+        Integer value = _directory.getInteger(JpegDirectory.TAG_COMPRESSION_TYPE);
+        if (value==null)
+            return null;
+        // Note there is no 2 or 12
+        switch (value) {
+            case 0: return "Baseline";
+            case 1: return "Extended sequential, Huffman";
+            case 2: return "Progressive, Huffman";
+            case 3: return "Lossless, Huffman";
+            case 5: return "Differential sequential, Huffman";
+            case 6: return "Differential progressive, Huffman";
+            case 7: return "Differential lossless, Huffman";
+            case 8: return "Reserved for JPEG extensions";
+            case 9: return "Extended sequential, arithmetic";
+            case 10: return "Progressive, arithmetic";
+            case 11: return "Lossless, arithmetic";
+            case 13: return "Differential sequential, arithmetic";
+            case 14: return "Differential progressive, arithmetic";
+            case 15: return "Differential lossless, arithmetic";
+            default:
+                return "Unknown type: "+ value;
+        }
+    }
+    @Nullable
+    public String getImageWidthDescription()
+    {
+        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_WIDTH);
+        if (value==null)
+            return null;
+        return value + " pixels";
+    }
+
+    @Nullable
+    public String getImageHeightDescription()
+    {
+        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_HEIGHT);
+        if (value==null)
+            return null;
+        return value + " pixels";
+    }
+
+    @Nullable
+    public String getDataPrecisionDescription()
+    {
+        final String value = _directory.getString(JpegDirectory.TAG_DATA_PRECISION);
+        if (value==null)
+            return null;
+        return value + " bits";
+    }
+
+    @Nullable
+    public String getComponentDataDescription(int componentNumber)
+    {
+        JpegComponent value = _directory.getComponent(componentNumber);
+
+        if (value==null)
+            return null;
+
+        return value.getComponentName() + " component: Quantization table " + value.getQuantizationTableNumber()
+            + ", Sampling factors " + value.getHorizontalSamplingFactor()
+            + " horiz/" + value.getVerticalSamplingFactor() + " vert";
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/JpegDirectory.java b/src/com/drew/metadata/jpeg/JpegDirectory.java
new file mode 100644
index 0000000..37eb488
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegDirectory.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+import com.drew.metadata.MetadataException;
+
+import java.util.HashMap;
+
+/**
+ * Directory of tags and values for the SOF0 JPEG segment.  This segment holds basic metadata about the image.
+ *
+ * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes https://drewnoakes.com
+ */
+public class JpegDirectory extends Directory
+{
+    public static final int TAG_COMPRESSION_TYPE = -3;
+    /** This is in bits/sample, usually 8 (12 and 16 not supported by most software). */
+    public static final int TAG_DATA_PRECISION = 0;
+    /** The image's height.  Necessary for decoding the image, so it should always be there. */
+    public static final int TAG_IMAGE_HEIGHT = 1;
+    /** The image's width.  Necessary for decoding the image, so it should always be there. */
+    public static final int TAG_IMAGE_WIDTH = 3;
+    /**
+     * Usually 1 = grey scaled, 3 = color YcbCr or YIQ, 4 = color CMYK
+     * Each component TAG_COMPONENT_DATA_[1-4], has the following meaning:
+     * component Id(1byte)(1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q),
+     * sampling factors (1byte) (bit 0-3 vertical., 4-7 horizontal.),
+     * quantization table number (1 byte).
+     * <p>
+     * This info is from http://www.funducode.com/freec/Fileformats/format3/format3b.htm
+     */
+    public static final int TAG_NUMBER_OF_COMPONENTS = 5;
+
+    // NOTE!  Component tag type int values must increment in steps of 1
+
+    /** the first of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_1 = 6;
+    /** the second of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_2 = 7;
+    /** the third of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_3 = 8;
+    /** the fourth of a possible 4 color components.  Number of components specified in TAG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_COMPONENT_DATA_4 = 9;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_COMPRESSION_TYPE, "Compression Type");
+        _tagNameMap.put(TAG_DATA_PRECISION, "Data Precision");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_NUMBER_OF_COMPONENTS, "Number of Components");
+        _tagNameMap.put(TAG_COMPONENT_DATA_1, "Component 1");
+        _tagNameMap.put(TAG_COMPONENT_DATA_2, "Component 2");
+        _tagNameMap.put(TAG_COMPONENT_DATA_3, "Component 3");
+        _tagNameMap.put(TAG_COMPONENT_DATA_4, "Component 4");
+    }
+
+    public JpegDirectory()
+    {
+        this.setDescriptor(new JpegDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "JPEG";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * @param componentNumber The zero-based index of the component.  This number is normally between 0 and 3.
+     *                        Use getNumberOfComponents for bounds-checking.
+     * @return the JpegComponent having the specified number.
+     */
+    @Nullable
+    public JpegComponent getComponent(int componentNumber)
+    {
+        int tagType = JpegDirectory.TAG_COMPONENT_DATA_1 + componentNumber;
+        return (JpegComponent)getObject(tagType);
+    }
+
+    public int getImageWidth() throws MetadataException
+    {
+        return getInt(JpegDirectory.TAG_IMAGE_WIDTH);
+    }
+
+    public int getImageHeight() throws MetadataException
+    {
+        return getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
+    }
+
+    public int getNumberOfComponents() throws MetadataException
+    {
+        return getInt(JpegDirectory.TAG_NUMBER_OF_COMPONENTS);
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/JpegReader.java b/src/com/drew/metadata/jpeg/JpegReader.java
new file mode 100644
index 0000000..e496718
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/JpegReader.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Decodes JPEG SOFn data, populating a {@link Metadata} object with tag values in a {@link JpegDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Darrell Silver http://www.darrellsilver.com
+ */
+public class JpegReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        // NOTE that some SOFn values do not exist
+        return Arrays.asList(
+            JpegSegmentType.SOF0,
+            JpegSegmentType.SOF1,
+            JpegSegmentType.SOF2,
+            JpegSegmentType.SOF3,
+//            JpegSegmentType.SOF4,
+            JpegSegmentType.SOF5,
+            JpegSegmentType.SOF6,
+            JpegSegmentType.SOF7,
+            JpegSegmentType.SOF8,
+            JpegSegmentType.SOF9,
+            JpegSegmentType.SOF10,
+            JpegSegmentType.SOF11,
+//            JpegSegmentType.SOF12,
+            JpegSegmentType.SOF13,
+            JpegSegmentType.SOF14,
+            JpegSegmentType.SOF15
+        );
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            extract(segmentBytes, metadata, segmentType);
+        }
+    }
+
+    public void extract(byte[] segmentBytes, Metadata metadata, JpegSegmentType segmentType)
+    {
+        JpegDirectory directory = new JpegDirectory();
+        metadata.addDirectory(directory);
+
+        // The value of TAG_COMPRESSION_TYPE is determined by the segment type found
+        directory.setInt(JpegDirectory.TAG_COMPRESSION_TYPE, segmentType.byteValue - JpegSegmentType.SOF0.byteValue);
+
+        SequentialReader reader = new SequentialByteArrayReader(segmentBytes);
+
+        try {
+            directory.setInt(JpegDirectory.TAG_DATA_PRECISION, reader.getUInt8());
+            directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
+            directory.setInt(JpegDirectory.TAG_IMAGE_WIDTH, reader.getUInt16());
+            short componentCount = reader.getUInt8();
+            directory.setInt(JpegDirectory.TAG_NUMBER_OF_COMPONENTS, componentCount);
+
+            // for each component, there are three bytes of data:
+            // 1 - Component ID: 1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q
+            // 2 - Sampling factors: bit 0-3 vertical, 4-7 horizontal
+            // 3 - Quantization table number
+            for (int i = 0; i < (int)componentCount; i++) {
+                final int componentId = reader.getUInt8();
+                final int samplingFactorByte = reader.getUInt8();
+                final int quantizationTableNumber = reader.getUInt8();
+                final JpegComponent component = new JpegComponent(componentId, samplingFactorByte, quantizationTableNumber);
+                directory.setObject(JpegDirectory.TAG_COMPONENT_DATA_1 + i, component);
+            }
+        } catch (IOException ex) {
+            directory.addError(ex.getMessage());
+        }
+    }
+}
diff --git a/src/com/drew/metadata/jpeg/package.html b/src/com/drew/metadata/jpeg/package.html
new file mode 100644
index 0000000..5e9b32a
--- /dev/null
+++ b/src/com/drew/metadata/jpeg/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of JPEG file format metadata.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/package.html b/src/com/drew/metadata/package.html
new file mode 100644
index 0000000..f7b7916
--- /dev/null
+++ b/src/com/drew/metadata/package.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Provides classes for generic modelling of metadata directories and tags.  Contains base types for metadata processing abstraction.
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/drew/metadata/tiff/DirectoryTiffHandler.java b/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
new file mode 100644
index 0000000..4fdae83
--- /dev/null
+++ b/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.tiff;
+
+import com.drew.imaging.tiff.TiffHandler;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+
+import java.util.Stack;
+
+/**
+ * Adapter between the {@link TiffHandler} interface and the {@link Metadata}/{@link Directory} object model.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class DirectoryTiffHandler implements TiffHandler
+{
+    private final Stack<Directory> _directoryStack = new Stack<Directory>();
+
+    protected Directory _currentDirectory;
+    protected final Metadata _metadata;
+
+    protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectoryClass)
+    {
+        _metadata = metadata;
+        try {
+            _currentDirectory = initialDirectoryClass.newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        _metadata.addDirectory(_currentDirectory);
+    }
+
+    public void endingIFD()
+    {
+        _currentDirectory = _directoryStack.empty() ? null : _directoryStack.pop();
+    }
+
+    protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
+    {
+        _directoryStack.push(_currentDirectory);
+        try {
+            _currentDirectory = directoryClass.newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        _metadata.addDirectory(_currentDirectory);
+    }
+
+    public void warn(@NotNull String message)
+    {
+        _currentDirectory.addError(message);
+    }
+
+    public void error(@NotNull String message)
+    {
+        _currentDirectory.addError(message);
+    }
+
+    public void setByteArray(int tagId, @NotNull byte[] bytes)
+    {
+        _currentDirectory.setByteArray(tagId, bytes);
+    }
+
+    public void setString(int tagId, @NotNull String string)
+    {
+        _currentDirectory.setString(tagId, string);
+    }
+
+    public void setRational(int tagId, @NotNull Rational rational)
+    {
+        _currentDirectory.setRational(tagId, rational);
+    }
+
+    public void setRationalArray(int tagId, @NotNull Rational[] array)
+    {
+        _currentDirectory.setRationalArray(tagId, array);
+    }
+
+    public void setFloat(int tagId, float float32)
+    {
+        _currentDirectory.setFloat(tagId, float32);
+    }
+
+    public void setFloatArray(int tagId, @NotNull float[] array)
+    {
+        _currentDirectory.setFloatArray(tagId, array);
+    }
+
+    public void setDouble(int tagId, double double64)
+    {
+        _currentDirectory.setDouble(tagId, double64);
+    }
+
+    public void setDoubleArray(int tagId, @NotNull double[] array)
+    {
+        _currentDirectory.setDoubleArray(tagId, array);
+    }
+
+    public void setInt8s(int tagId, byte int8s)
+    {
+        // NOTE Directory stores all integral types as int32s, except for int32u and long
+        _currentDirectory.setInt(tagId, int8s);
+    }
+
+    public void setInt8sArray(int tagId, @NotNull byte[] array)
+    {
+        // NOTE Directory stores all integral types as int32s, except for int32u and long
+        _currentDirectory.setByteArray(tagId, array);
+    }
+
+    public void setInt8u(int tagId, short int8u)
+    {
+        // NOTE Directory stores all integral types as int32s, except for int32u and long
+        _currentDirectory.setInt(tagId, int8u);
+    }
+
+    public void setInt8uArray(int tagId, @NotNull short[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+
+    public void setInt16s(int tagId, int int16s)
+    {
+        // TODO create and use a proper setter for int16u?
+        _currentDirectory.setInt(tagId, int16s);
+    }
+
+    public void setInt16sArray(int tagId, @NotNull short[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+
+    public void setInt16u(int tagId, int int16u)
+    {
+        // TODO create and use a proper setter for
+        _currentDirectory.setInt(tagId, int16u);
+    }
+
+    public void setInt16uArray(int tagId, @NotNull int[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+
+    public void setInt32s(int tagId, int int32s)
+    {
+        _currentDirectory.setInt(tagId, int32s);
+    }
+
+    public void setInt32sArray(int tagId, @NotNull int[] array)
+    {
+        _currentDirectory.setIntArray(tagId, array);
+    }
+
+    public void setInt32u(int tagId, long int32u)
+    {
+        _currentDirectory.setLong(tagId, int32u);
+    }
+
+    public void setInt32uArray(int tagId, @NotNull long[] array)
+    {
+        // TODO create and use a proper setter for short[]
+        _currentDirectory.setObjectArray(tagId, array);
+    }
+}
diff --git a/src/com/drew/metadata/tiff/package.html b/src/com/drew/metadata/tiff/package.html
new file mode 100644
index 0000000..b0cc736
--- /dev/null
+++ b/src/com/drew/metadata/tiff/package.html
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of TIFF file metadata.
+
+<!-- Put @see and @since tags down here. -->
+ at since 2.7.0
+
+</body>
+</html>

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/josm.git



More information about the Pkg-grass-devel mailing list