From d38c0a546ee34d383f28b6d1fcddc0511d7312bc Mon Sep 17 00:00:00 2001
From: Markus Koschany <apo@debian.org>
Date: Fri, 30 Jun 2017 21:38:10 +0200
Subject: [PATCH] New upstream version 2.10.1

---
 CONTRIBUTING.md                               |   30 -
 LICENSE-2.0.txt                               |    2 +-
 README.md                                     |  102 +-
 Resources/favicon-16px.png                    |  Bin 0 -> 590 bytes
 Resources/favicon-256.ico                     |  Bin 0 -> 9979 bytes
 Resources/favicon-256px.png                   |  Bin 0 -> 7111 bytes
 Resources/favicon-32px.png                    |  Bin 0 -> 1014 bytes
 Resources/favicon-48.ico                      |  Bin 0 -> 2926 bytes
 Resources/favicon-48px.png                    |  Bin 0 -> 1490 bytes
 Resources/favicon.xcf                         |  Bin 0 -> 32098 bytes
 Resources/metadata-extractor-logo-square.svg  |   69 +
 Resources/metadata-extractor-logo.svg         |    8 +
 .../com/drew/metadata/GeoTagMapBuilder.java   |   27 +-
 Samples/com/drew/metadata/SampleUsage.java    |    4 +-
 Samples/com/drew/metadata/XmpSample.java      |   70 +
 Source/com/drew/imaging/FileType.java         |    4 +-
 Source/com/drew/imaging/FileTypeDetector.java |   12 +-
 .../com/drew/imaging/ImageMetadataReader.java |  101 +-
 .../imaging/ImageProcessingException.java     |    2 +-
 .../drew/imaging/PhotographicConversions.java |    2 +-
 .../drew/imaging/bmp/BmpMetadataReader.java   |    2 +-
 Source/com/drew/imaging/bmp/package-info.java |    6 +
 Source/com/drew/imaging/bmp/package.html      |   34 -
 .../drew/imaging/gif/GifMetadataReader.java   |   15 +-
 Source/com/drew/imaging/gif/package-info.java |    4 +
 Source/com/drew/imaging/gif/package.html      |   33 -
 .../drew/imaging/ico/IcoMetadataReader.java   |   59 +
 Source/com/drew/imaging/ico/package-info.java |    4 +
 .../drew/imaging/jpeg/JpegMetadataReader.java |   33 +-
 .../imaging/jpeg/JpegProcessingException.java |    2 +-
 .../drew/imaging/jpeg/JpegSegmentData.java    |    2 +-
 .../jpeg/JpegSegmentMetadataReader.java       |   16 +-
 .../drew/imaging/jpeg/JpegSegmentReader.java  |   24 +-
 .../drew/imaging/jpeg/JpegSegmentType.java    |   66 +-
 .../com/drew/imaging/jpeg/package-info.java   |    4 +
 Source/com/drew/imaging/jpeg/package.html     |   33 -
 Source/com/drew/imaging/package-info.java     |    5 +
 Source/com/drew/imaging/package.html          |   33 -
 .../drew/imaging/pcx/PcxMetadataReader.java   |   59 +
 Source/com/drew/imaging/pcx/package-info.java |    4 +
 .../drew/imaging/png/PngChromaticities.java   |   20 +
 Source/com/drew/imaging/png/PngChunk.java     |   20 +
 .../com/drew/imaging/png/PngChunkReader.java  |   20 +
 Source/com/drew/imaging/png/PngChunkType.java |   93 +-
 Source/com/drew/imaging/png/PngColorType.java |   20 +
 Source/com/drew/imaging/png/PngHeader.java    |   20 +
 .../drew/imaging/png/PngMetadataReader.java   |  397 +++--
 .../imaging/png/PngProcessingException.java   |    2 +-
 Source/com/drew/imaging/png/package-info.java |    6 +
 Source/com/drew/imaging/png/package.html      |   34 -
 .../drew/imaging/psd/PsdMetadataReader.java   |   18 +-
 Source/com/drew/imaging/psd/package-info.java |    4 +
 Source/com/drew/imaging/psd/package.html      |   33 -
 .../drew/imaging/raf/RafMetadataReader.java   |   72 +
 Source/com/drew/imaging/raf/package-info.java |    4 +
 Source/com/drew/imaging/riff/RiffHandler.java |   65 +
 .../imaging/riff/RiffProcessingException.java |   50 +
 Source/com/drew/imaging/riff/RiffReader.java  |  101 ++
 .../com/drew/imaging/riff/package-info.java   |    4 +
 .../com/drew/imaging/tiff/TiffDataFormat.java |    2 +-
 Source/com/drew/imaging/tiff/TiffHandler.java |   14 +-
 .../drew/imaging/tiff/TiffMetadataReader.java |   23 +-
 .../imaging/tiff/TiffProcessingException.java |    2 +-
 Source/com/drew/imaging/tiff/TiffReader.java  |   84 +-
 .../com/drew/imaging/tiff/package-info.java   |    4 +
 Source/com/drew/imaging/tiff/package.html     |   33 -
 .../drew/imaging/webp/WebpMetadataReader.java |   61 +
 .../com/drew/imaging/webp/package-info.java   |    4 +
 .../com/drew/lang/BufferBoundsException.java  |    2 +-
 Source/com/drew/lang/ByteArrayReader.java     |   32 +-
 Source/com/drew/lang/ByteConvert.java         |   45 +
 Source/com/drew/lang/ByteTrie.java            |    2 +-
 Source/com/drew/lang/Charsets.java            |   40 +
 Source/com/drew/lang/CompoundException.java   |    2 +-
 Source/com/drew/lang/DateUtil.java            |   32 +
 Source/com/drew/lang/GeoLocation.java         |    4 +-
 Source/com/drew/lang/KeyValuePair.java        |   29 +-
 Source/com/drew/lang/NullOutputStream.java    |    2 +-
 .../com/drew/lang/RandomAccessFileReader.java |   20 +-
 Source/com/drew/lang/RandomAccessReader.java  |  102 +-
 .../drew/lang/RandomAccessStreamReader.java   |   36 +-
 Source/com/drew/lang/Rational.java            |  119 +-
 .../drew/lang/SequentialByteArrayReader.java  |   35 +-
 Source/com/drew/lang/SequentialReader.java    |   95 +-
 Source/com/drew/lang/StreamReader.java        |   40 +-
 Source/com/drew/lang/StreamUtil.java          |   46 +
 Source/com/drew/lang/StringUtil.java          |    4 +-
 Source/com/drew/lang/annotations/NotNull.java |    2 +-
 .../com/drew/lang/annotations/Nullable.java   |    2 +-
 .../drew/lang/annotations/package-info.java   |    5 +
 Source/com/drew/lang/annotations/package.html |   34 -
 Source/com/drew/lang/package-info.java        |    4 +
 Source/com/drew/lang/package.html             |   33 -
 Source/com/drew/metadata/Age.java             |    7 +-
 Source/com/drew/metadata/Directory.java       |  295 +++-
 ...TagDescriptor.java => ErrorDirectory.java} |   54 +-
 Source/com/drew/metadata/Face.java            |    2 +-
 Source/com/drew/metadata/Metadata.java        |  100 +-
 .../com/drew/metadata/MetadataException.java  |    2 +-
 Source/com/drew/metadata/MetadataReader.java  |    4 +-
 Source/com/drew/metadata/Schema.java          |   41 +
 Source/com/drew/metadata/StringValue.java     |   76 +
 Source/com/drew/metadata/Tag.java             |   12 +-
 Source/com/drew/metadata/TagDescriptor.java   |  202 ++-
 .../metadata/adobe/AdobeJpegDescriptor.java   |   24 +-
 .../metadata/adobe/AdobeJpegDirectory.java    |    3 +-
 .../drew/metadata/adobe/AdobeJpegReader.java  |   30 +-
 .../com/drew/metadata/adobe/package-info.java |    4 +
 Source/com/drew/metadata/adobe/package.html   |   33 -
 .../metadata/bmp/BmpHeaderDescriptor.java     |   31 +-
 .../drew/metadata/bmp/BmpHeaderDirectory.java |   21 +
 Source/com/drew/metadata/bmp/BmpReader.java   |   23 +-
 .../com/drew/metadata/bmp/package-info.java   |    6 +
 Source/com/drew/metadata/bmp/package.html     |   34 -
 .../metadata/exif/ExifDescriptorBase.java     | 1234 ++++++++++++++
 .../drew/metadata/exif/ExifDirectoryBase.java |  764 +++++++++
 .../metadata/exif/ExifIFD0Descriptor.java     |  178 +--
 .../drew/metadata/exif/ExifIFD0Directory.java |   74 +-
 .../metadata/exif/ExifImageDescriptor.java    |   37 +
 .../metadata/exif/ExifImageDirectory.java     |   64 +
 .../metadata/exif/ExifInteropDescriptor.java  |   42 +-
 .../metadata/exif/ExifInteropDirectory.java   |   18 +-
 Source/com/drew/metadata/exif/ExifReader.java |   80 +-
 .../metadata/exif/ExifSubIFDDescriptor.java   |  804 +---------
 .../metadata/exif/ExifSubIFDDirectory.java    |  676 +-------
 .../exif/ExifThumbnailDescriptor.java         |  264 +--
 .../metadata/exif/ExifThumbnailDirectory.java |  355 +----
 .../drew/metadata/exif/ExifTiffHandler.java   |  602 ++++++-
 .../com/drew/metadata/exif/GpsDescriptor.java |   13 +-
 .../com/drew/metadata/exif/GpsDirectory.java  |   49 +-
 .../PanasonicRawDistortionDescriptor.java     |  159 ++
 .../exif/PanasonicRawDistortionDirectory.java |   86 +
 .../exif/PanasonicRawIFD0Descriptor.java      |   58 +
 .../exif/PanasonicRawIFD0Directory.java       |  153 ++
 .../exif/PanasonicRawWbInfo2Descriptor.java   |   71 +
 .../exif/PanasonicRawWbInfo2Directory.java    |  103 ++
 .../exif/PanasonicRawWbInfoDescriptor.java    |   71 +
 .../exif/PanasonicRawWbInfoDirectory.java     |  102 ++
 .../drew/metadata/exif/PrintIMDescriptor.java |   58 +
 .../drew/metadata/exif/PrintIMDirectory.java  |   67 +
 .../makernotes/AppleMakernoteDescriptor.java  |   59 +
 .../makernotes/AppleMakernoteDirectory.java   |   70 +
 .../makernotes/CanonMakernoteDescriptor.java  |  461 +++++-
 .../makernotes/CanonMakernoteDirectory.java   |  110 +-
 .../CasioType1MakernoteDescriptor.java        |    9 +-
 .../CasioType1MakernoteDirectory.java         |    3 +-
 .../CasioType2MakernoteDescriptor.java        |   16 +-
 .../CasioType2MakernoteDirectory.java         |    3 +-
 .../FujifilmMakernoteDescriptor.java          |    3 +-
 .../FujifilmMakernoteDirectory.java           |    3 +-
 .../makernotes/KodakMakernoteDescriptor.java  |    3 +-
 .../makernotes/KodakMakernoteDirectory.java   |    3 +-
 .../KyoceraMakernoteDescriptor.java           |   11 +-
 .../makernotes/KyoceraMakernoteDirectory.java |    3 +-
 .../makernotes/LeicaMakernoteDescriptor.java  |    3 +-
 .../makernotes/LeicaMakernoteDirectory.java   |    3 +-
 .../LeicaType5MakernoteDescriptor.java        |   79 +
 .../LeicaType5MakernoteDirectory.java         |   79 +
 .../NikonType1MakernoteDescriptor.java        |    3 +-
 .../NikonType1MakernoteDirectory.java         |    3 +-
 .../NikonType2MakernoteDescriptor.java        |   14 +-
 .../NikonType2MakernoteDirectory.java         |    7 +-
 ...mpusCameraSettingsMakernoteDescriptor.java | 1412 +++++++++++++++++
 ...ympusCameraSettingsMakernoteDirectory.java |  201 +++
 .../OlympusEquipmentMakernoteDescriptor.java  |  386 +++++
 .../OlympusEquipmentMakernoteDirectory.java   |  118 ++
 .../OlympusFocusInfoMakernoteDescriptor.java  |  217 +++
 .../OlympusFocusInfoMakernoteDirectory.java   |  110 ++
 ...pusImageProcessingMakernoteDescriptor.java |  240 +++
 ...mpusImageProcessingMakernoteDirectory.java |  212 +++
 .../OlympusMakernoteDescriptor.java           |  309 +++-
 .../makernotes/OlympusMakernoteDirectory.java |  508 +++++-
 ...pusRawDevelopment2MakernoteDescriptor.java |  230 +++
 ...mpusRawDevelopment2MakernoteDirectory.java |  111 ++
 ...mpusRawDevelopmentMakernoteDescriptor.java |  155 ++
 ...ympusRawDevelopmentMakernoteDirectory.java |   91 ++
 .../OlympusRawInfoMakernoteDescriptor.java    |  141 ++
 .../OlympusRawInfoMakernoteDirectory.java     |  142 ++
 .../PanasonicMakernoteDescriptor.java         |  305 +++-
 .../PanasonicMakernoteDirectory.java          |  126 +-
 .../makernotes/PentaxMakernoteDescriptor.java |    3 +-
 .../makernotes/PentaxMakernoteDirectory.java  |    3 +-
 .../ReconyxHyperFireMakernoteDescriptor.java  |  105 ++
 .../ReconyxHyperFireMakernoteDirectory.java   |  105 ++
 .../ReconyxUltraFireMakernoteDescriptor.java  |  110 ++
 .../ReconyxUltraFireMakernoteDirectory.java   |  116 ++
 .../makernotes/RicohMakernoteDescriptor.java  |   13 +-
 .../makernotes/RicohMakernoteDirectory.java   |    3 +-
 .../SamsungType2MakernoteDescriptor.java      |  213 +++
 .../SamsungType2MakernoteDirectory.java       |   89 ++
 .../makernotes/SanyoMakernoteDescriptor.java  |    3 +-
 .../makernotes/SanyoMakernoteDirectory.java   |    7 +-
 .../makernotes/SigmaMakernoteDescriptor.java  |    3 +-
 .../makernotes/SigmaMakernoteDirectory.java   |    3 +-
 .../SonyType1MakernoteDescriptor.java         |   33 +-
 .../SonyType1MakernoteDirectory.java          |    5 +-
 .../SonyType6MakernoteDescriptor.java         |    3 +-
 .../SonyType6MakernoteDirectory.java          |    3 +-
 .../exif/makernotes/package-info.java         |    5 +
 .../metadata/exif/makernotes/package.html     |   33 -
 .../com/drew/metadata/exif/package-info.java  |    4 +
 Source/com/drew/metadata/exif/package.html    |   33 -
 .../metadata/file/FileMetadataDescriptor.java |   63 +
 .../metadata/file/FileMetadataDirectory.java  |   65 +
 .../metadata/file/FileMetadataReader.java     |   49 +
 .../com/drew/metadata/file/package-info.java  |    6 +
 .../metadata/gif/GifAnimationDescriptor.java  |   62 +
 .../metadata/gif/GifAnimationDirectory.java   |   63 +
 .../metadata/gif/GifCommentDescriptor.java    |   37 +
 .../metadata/gif/GifCommentDirectory.java     |   65 +
 .../metadata/gif/GifControlDescriptor.java    |   37 +
 .../metadata/gif/GifControlDirectory.java     |  134 ++
 .../metadata/gif/GifHeaderDescriptor.java     |   21 +
 .../drew/metadata/gif/GifHeaderDirectory.java |   28 +-
 .../drew/metadata/gif/GifImageDescriptor.java |   37 +
 .../drew/metadata/gif/GifImageDirectory.java  |   77 +
 Source/com/drew/metadata/gif/GifReader.java   |  381 ++++-
 .../com/drew/metadata/gif/package-info.java   |    6 +
 Source/com/drew/metadata/gif/package.html     |   34 -
 .../com/drew/metadata/icc/IccDescriptor.java  |   54 +-
 .../com/drew/metadata/icc/IccDirectory.java   |    3 +-
 Source/com/drew/metadata/icc/IccReader.java   |   88 +-
 .../com/drew/metadata/icc/package-info.java   |    4 +
 Source/com/drew/metadata/icc/package.html     |   33 -
 .../com/drew/metadata/ico/IcoDescriptor.java  |   90 ++
 .../com/drew/metadata/ico/IcoDirectory.java   |   80 +
 Source/com/drew/metadata/ico/IcoReader.java   |  110 ++
 .../com/drew/metadata/ico/package-info.java   |    4 +
 .../drew/metadata/iptc/IptcDescriptor.java    |  145 +-
 .../com/drew/metadata/iptc/IptcDirectory.java |   84 +-
 Source/com/drew/metadata/iptc/IptcReader.java |  113 +-
 .../drew/metadata/iptc/Iso2022Converter.java  |   39 +-
 .../com/drew/metadata/iptc/package-info.java  |    4 +
 Source/com/drew/metadata/iptc/package.html    |   33 -
 .../drew/metadata/jfif/JfifDescriptor.java    |   28 +-
 .../com/drew/metadata/jfif/JfifDirectory.java |   24 +-
 Source/com/drew/metadata/jfif/JfifReader.java |   49 +-
 .../com/drew/metadata/jfif/package-info.java  |    4 +
 Source/com/drew/metadata/jfif/package.html    |   33 -
 .../drew/metadata/jfxx/JfxxDescriptor.java    |   72 +
 .../com/drew/metadata/jfxx/JfxxDirectory.java |   70 +
 Source/com/drew/metadata/jfxx/JfxxReader.java |   80 +
 .../com/drew/metadata/jfxx/package-info.java  |    4 +
 .../jpeg/HuffmanTablesDescriptor.java         |   67 +
 .../metadata/jpeg/HuffmanTablesDirectory.java |  360 +++++
 .../metadata/jpeg/JpegCommentDescriptor.java  |    3 +-
 .../metadata/jpeg/JpegCommentDirectory.java   |    3 +-
 .../drew/metadata/jpeg/JpegCommentReader.java |   24 +-
 .../com/drew/metadata/jpeg/JpegComponent.java |   24 +-
 .../drew/metadata/jpeg/JpegDescriptor.java    |   71 +-
 .../com/drew/metadata/jpeg/JpegDhtReader.java |  101 ++
 .../com/drew/metadata/jpeg/JpegDirectory.java |    3 +-
 .../com/drew/metadata/jpeg/JpegDnlReader.java |   77 +
 Source/com/drew/metadata/jpeg/JpegReader.java |   22 +-
 .../com/drew/metadata/jpeg/package-info.java  |    4 +
 Source/com/drew/metadata/jpeg/package.html    |   33 -
 Source/com/drew/metadata/package-info.java    |    6 +
 Source/com/drew/metadata/package.html         |   33 -
 .../com/drew/metadata/pcx/PcxDescriptor.java  |   86 +
 .../com/drew/metadata/pcx/PcxDirectory.java   |   87 +
 Source/com/drew/metadata/pcx/PcxReader.java   |   87 +
 .../com/drew/metadata/pcx/package-info.java   |    4 +
 .../metadata/photoshop/DuckyDirectory.java    |   69 +
 .../drew/metadata/photoshop/DuckyReader.java  |  116 ++
 .../photoshop/PhotoshopDescriptor.java        |   96 +-
 .../photoshop/PhotoshopDirectory.java         |  177 ++-
 .../metadata/photoshop/PhotoshopReader.java   |  120 +-
 .../photoshop/PsdHeaderDescriptor.java        |   79 +-
 .../photoshop/PsdHeaderDirectory.java         |    3 +-
 .../drew/metadata/photoshop/PsdReader.java    |   70 +-
 .../drew/metadata/photoshop/package-info.java |    4 +
 .../com/drew/metadata/photoshop/package.html  |   33 -
 .../png/PngChromaticitiesDirectory.java       |   21 +
 .../com/drew/metadata/png/PngDescriptor.java  |   73 +-
 .../com/drew/metadata/png/PngDirectory.java   |   46 +-
 .../com/drew/metadata/png/package-info.java   |    6 +
 Source/com/drew/metadata/png/package.html     |   34 -
 .../metadata/tiff/DirectoryTiffHandler.java   |   51 +-
 .../com/drew/metadata/tiff/package-info.java  |    6 +
 Source/com/drew/metadata/tiff/package.html    |   34 -
 .../drew/metadata/webp/WebpDescriptor.java    |   47 +
 .../com/drew/metadata/webp/WebpDirectory.java |   67 +
 .../drew/metadata/webp/WebpRiffHandler.java   |  164 ++
 .../com/drew/metadata/webp/package-info.java  |    6 +
 .../com/drew/metadata/xmp/XmpDescriptor.java  |  140 +-
 .../com/drew/metadata/xmp/XmpDirectory.java   |  153 +-
 Source/com/drew/metadata/xmp/XmpReader.java   |  344 ++--
 Source/com/drew/metadata/xmp/XmpWriter.java   |   37 +
 .../com/drew/metadata/xmp/package-info.java   |    4 +
 Source/com/drew/metadata/xmp/package.html     |   33 -
 .../drew/tools/ExtractJpegSegmentTool.java    |    4 +-
 Source/com/drew/tools/FileUtil.java           |    2 +-
 .../ProcessAllImagesInFolderUtility.java      |  384 ++++-
 Source/com/drew/tools/ProcessUrlUtility.java  |    4 +-
 Source/com/drew/tools/package-info.java       |    5 +
 Source/com/drew/tools/package.html            |   33 -
 Tests/Data/withTypicalHuffman.jpg             |  Bin 0 -> 1570 bytes
 Tests/Data/withXmp.jpg                        |  Bin 0 -> 12666 bytes
 .../imaging/jpeg/JpegMetadataReaderTest.java  |   38 +-
 .../imaging/jpeg/JpegSegmentDataTest.java     |    2 +-
 .../imaging/jpeg/JpegSegmentReaderTest.java   |   32 +-
 .../drew/imaging/png/PngChunkReaderTest.java  |   20 +
 .../drew/imaging/png/PngChunkTypeTest.java    |   26 +-
 .../imaging/png/PngMetadataReaderTest.java    |   90 +-
 Tests/com/drew/lang/ByteArrayReaderTest.java  |    2 +-
 Tests/com/drew/lang/ByteConvertTest.java      |   25 +
 Tests/com/drew/lang/ByteTrieTest.java         |    2 +-
 .../com/drew/lang/CompoundExceptionTest.java  |    2 +-
 Tests/com/drew/lang/GeoLocationTest.java      |    2 +-
 Tests/com/drew/lang/NullOutputStreamTest.java |    2 +-
 .../drew/lang/RandomAccessFileReaderTest.java |    7 +-
 .../lang/RandomAccessStreamReaderTest.java    |    2 +-
 Tests/com/drew/lang/RandomAccessTestBase.java |   46 +-
 Tests/com/drew/lang/RationalTest.java         |  138 +-
 .../drew/lang/SequentialAccessTestBase.java   |   12 +-
 .../lang/SequentialByteArrayReaderTest.java   |   20 +
 Tests/com/drew/lang/StreamReaderTest.java     |   20 +
 Tests/com/drew/lang/StringUtilTest.java       |    2 +-
 Tests/com/drew/metadata/AgeTest.java          |    2 +-
 Tests/com/drew/metadata/DirectoryTest.java    |   89 +-
 Tests/com/drew/metadata/MetadataTest.java     |   92 +-
 Tests/com/drew/metadata/MockDirectory.java    |    2 +-
 .../metadata/adobe/AdobeJpegReaderTest.java   |    4 +-
 .../com/drew/metadata/bmp/BmpReaderTest.java  |    4 +-
 .../exif/CanonMakernoteDescriptorTest.java    |   43 +-
 .../drew/metadata/exif/ExifDirectoryTest.java |  112 +-
 .../metadata/exif/ExifIFD0DescriptorTest.java |   35 +-
 .../exif/ExifInteropDescriptorTest.java       |   11 +-
 .../drew/metadata/exif/ExifReaderTest.java    |   47 +-
 .../exif/ExifSubIFDDescriptorTest.java        |   31 +-
 .../exif/ExifThumbnailDescriptorTest.java     |   11 +-
 .../exif/NikonType1MakernoteTest.java         |   12 +-
 .../exif/NikonType2MakernoteTest1.java        |    2 +-
 .../exif/NikonType2MakernoteTest2.java        |   47 +-
 .../PanasonicMakernoteDescriptorTest.java     |    2 +-
 .../metadata/exif/SonyType1MakernoteTest.java |    2 +-
 .../metadata/exif/SonyType6MakernoteTest.java |    2 +-
 .../com/drew/metadata/gif/GifReaderTest.java  |    8 +-
 .../com/drew/metadata/icc/IccReaderTest.java  |   48 +-
 .../drew/metadata/iptc/IptcDirectoryTest.java |  109 ++
 .../drew/metadata/iptc/IptcReaderTest.java    |   71 +-
 .../metadata/iptc/Iso2022ConverterTest.java   |   24 +-
 .../drew/metadata/jfif/JfifReaderTest.java    |   12 +-
 .../jpeg/HuffmanTablesDescriptorTest.java     |   60 +
 .../jpeg/HuffmanTablesDirectoryTest.java      |   94 ++
 .../drew/metadata/jpeg/JpegComponentTest.java |    2 +-
 .../metadata/jpeg/JpegDescriptorTest.java     |   19 +-
 .../drew/metadata/jpeg/JpegDhtReaderTest.java |   99 ++
 .../drew/metadata/jpeg/JpegDirectoryTest.java |    2 +-
 .../drew/metadata/jpeg/JpegReaderTest.java    |    4 +-
 .../metadata/photoshop/PsdReaderTest.java     |   19 +-
 .../com/drew/metadata/xmp/XmpReaderTest.java  |  170 +-
 Tests/com/drew/testing/TestHelper.java        |    2 +-
 build.xml                                     |  192 ---
 pom.xml                                       |   67 +-
 355 files changed, 19034 insertions(+), 5872 deletions(-)
 delete mode 100644 CONTRIBUTING.md
 create mode 100644 Resources/favicon-16px.png
 create mode 100644 Resources/favicon-256.ico
 create mode 100644 Resources/favicon-256px.png
 create mode 100644 Resources/favicon-32px.png
 create mode 100644 Resources/favicon-48.ico
 create mode 100644 Resources/favicon-48px.png
 create mode 100644 Resources/favicon.xcf
 create mode 100644 Resources/metadata-extractor-logo-square.svg
 create mode 100644 Resources/metadata-extractor-logo.svg
 create mode 100644 Samples/com/drew/metadata/XmpSample.java
 create mode 100644 Source/com/drew/imaging/bmp/package-info.java
 delete mode 100644 Source/com/drew/imaging/bmp/package.html
 create mode 100644 Source/com/drew/imaging/gif/package-info.java
 delete mode 100644 Source/com/drew/imaging/gif/package.html
 create mode 100644 Source/com/drew/imaging/ico/IcoMetadataReader.java
 create mode 100644 Source/com/drew/imaging/ico/package-info.java
 create mode 100644 Source/com/drew/imaging/jpeg/package-info.java
 delete mode 100644 Source/com/drew/imaging/jpeg/package.html
 create mode 100644 Source/com/drew/imaging/package-info.java
 delete mode 100644 Source/com/drew/imaging/package.html
 create mode 100644 Source/com/drew/imaging/pcx/PcxMetadataReader.java
 create mode 100644 Source/com/drew/imaging/pcx/package-info.java
 create mode 100644 Source/com/drew/imaging/png/package-info.java
 delete mode 100644 Source/com/drew/imaging/png/package.html
 create mode 100644 Source/com/drew/imaging/psd/package-info.java
 delete mode 100644 Source/com/drew/imaging/psd/package.html
 create mode 100644 Source/com/drew/imaging/raf/RafMetadataReader.java
 create mode 100644 Source/com/drew/imaging/raf/package-info.java
 create mode 100644 Source/com/drew/imaging/riff/RiffHandler.java
 create mode 100644 Source/com/drew/imaging/riff/RiffProcessingException.java
 create mode 100644 Source/com/drew/imaging/riff/RiffReader.java
 create mode 100644 Source/com/drew/imaging/riff/package-info.java
 create mode 100644 Source/com/drew/imaging/tiff/package-info.java
 delete mode 100644 Source/com/drew/imaging/tiff/package.html
 create mode 100644 Source/com/drew/imaging/webp/WebpMetadataReader.java
 create mode 100644 Source/com/drew/imaging/webp/package-info.java
 create mode 100644 Source/com/drew/lang/ByteConvert.java
 create mode 100644 Source/com/drew/lang/Charsets.java
 create mode 100644 Source/com/drew/lang/DateUtil.java
 create mode 100644 Source/com/drew/lang/StreamUtil.java
 create mode 100644 Source/com/drew/lang/annotations/package-info.java
 delete mode 100644 Source/com/drew/lang/annotations/package.html
 create mode 100644 Source/com/drew/lang/package-info.java
 delete mode 100644 Source/com/drew/lang/package.html
 rename Source/com/drew/metadata/{DefaultTagDescriptor.java => ErrorDirectory.java} (51%)
 create mode 100644 Source/com/drew/metadata/Schema.java
 create mode 100644 Source/com/drew/metadata/StringValue.java
 create mode 100644 Source/com/drew/metadata/adobe/package-info.java
 delete mode 100644 Source/com/drew/metadata/adobe/package.html
 create mode 100644 Source/com/drew/metadata/bmp/package-info.java
 delete mode 100644 Source/com/drew/metadata/bmp/package.html
 create mode 100644 Source/com/drew/metadata/exif/ExifDescriptorBase.java
 create mode 100644 Source/com/drew/metadata/exif/ExifDirectoryBase.java
 create mode 100644 Source/com/drew/metadata/exif/ExifImageDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/ExifImageDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/PrintIMDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/PrintIMDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java
 create mode 100644 Source/com/drew/metadata/exif/makernotes/package-info.java
 delete mode 100644 Source/com/drew/metadata/exif/makernotes/package.html
 create mode 100644 Source/com/drew/metadata/exif/package-info.java
 delete mode 100644 Source/com/drew/metadata/exif/package.html
 create mode 100644 Source/com/drew/metadata/file/FileMetadataDescriptor.java
 create mode 100644 Source/com/drew/metadata/file/FileMetadataDirectory.java
 create mode 100644 Source/com/drew/metadata/file/FileMetadataReader.java
 create mode 100644 Source/com/drew/metadata/file/package-info.java
 create mode 100644 Source/com/drew/metadata/gif/GifAnimationDescriptor.java
 create mode 100644 Source/com/drew/metadata/gif/GifAnimationDirectory.java
 create mode 100644 Source/com/drew/metadata/gif/GifCommentDescriptor.java
 create mode 100644 Source/com/drew/metadata/gif/GifCommentDirectory.java
 create mode 100644 Source/com/drew/metadata/gif/GifControlDescriptor.java
 create mode 100644 Source/com/drew/metadata/gif/GifControlDirectory.java
 create mode 100644 Source/com/drew/metadata/gif/GifImageDescriptor.java
 create mode 100644 Source/com/drew/metadata/gif/GifImageDirectory.java
 create mode 100644 Source/com/drew/metadata/gif/package-info.java
 delete mode 100644 Source/com/drew/metadata/gif/package.html
 create mode 100644 Source/com/drew/metadata/icc/package-info.java
 delete mode 100644 Source/com/drew/metadata/icc/package.html
 create mode 100644 Source/com/drew/metadata/ico/IcoDescriptor.java
 create mode 100644 Source/com/drew/metadata/ico/IcoDirectory.java
 create mode 100644 Source/com/drew/metadata/ico/IcoReader.java
 create mode 100644 Source/com/drew/metadata/ico/package-info.java
 create mode 100644 Source/com/drew/metadata/iptc/package-info.java
 delete mode 100644 Source/com/drew/metadata/iptc/package.html
 create mode 100644 Source/com/drew/metadata/jfif/package-info.java
 delete mode 100644 Source/com/drew/metadata/jfif/package.html
 create mode 100644 Source/com/drew/metadata/jfxx/JfxxDescriptor.java
 create mode 100644 Source/com/drew/metadata/jfxx/JfxxDirectory.java
 create mode 100644 Source/com/drew/metadata/jfxx/JfxxReader.java
 create mode 100644 Source/com/drew/metadata/jfxx/package-info.java
 create mode 100644 Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java
 create mode 100644 Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java
 create mode 100644 Source/com/drew/metadata/jpeg/JpegDhtReader.java
 create mode 100644 Source/com/drew/metadata/jpeg/JpegDnlReader.java
 create mode 100644 Source/com/drew/metadata/jpeg/package-info.java
 delete mode 100644 Source/com/drew/metadata/jpeg/package.html
 create mode 100644 Source/com/drew/metadata/package-info.java
 delete mode 100644 Source/com/drew/metadata/package.html
 create mode 100644 Source/com/drew/metadata/pcx/PcxDescriptor.java
 create mode 100644 Source/com/drew/metadata/pcx/PcxDirectory.java
 create mode 100644 Source/com/drew/metadata/pcx/PcxReader.java
 create mode 100644 Source/com/drew/metadata/pcx/package-info.java
 create mode 100644 Source/com/drew/metadata/photoshop/DuckyDirectory.java
 create mode 100644 Source/com/drew/metadata/photoshop/DuckyReader.java
 create mode 100644 Source/com/drew/metadata/photoshop/package-info.java
 delete mode 100644 Source/com/drew/metadata/photoshop/package.html
 create mode 100644 Source/com/drew/metadata/png/package-info.java
 delete mode 100644 Source/com/drew/metadata/png/package.html
 create mode 100644 Source/com/drew/metadata/tiff/package-info.java
 delete mode 100644 Source/com/drew/metadata/tiff/package.html
 create mode 100644 Source/com/drew/metadata/webp/WebpDescriptor.java
 create mode 100644 Source/com/drew/metadata/webp/WebpDirectory.java
 create mode 100644 Source/com/drew/metadata/webp/WebpRiffHandler.java
 create mode 100644 Source/com/drew/metadata/webp/package-info.java
 create mode 100644 Source/com/drew/metadata/xmp/XmpWriter.java
 create mode 100644 Source/com/drew/metadata/xmp/package-info.java
 delete mode 100644 Source/com/drew/metadata/xmp/package.html
 create mode 100644 Source/com/drew/tools/package-info.java
 delete mode 100644 Source/com/drew/tools/package.html
 create mode 100644 Tests/Data/withTypicalHuffman.jpg
 create mode 100644 Tests/Data/withXmp.jpg
 create mode 100644 Tests/com/drew/lang/ByteConvertTest.java
 create mode 100644 Tests/com/drew/metadata/iptc/IptcDirectoryTest.java
 create mode 100644 Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java
 create mode 100644 Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java
 create mode 100644 Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java
 delete mode 100644 build.xml

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 1a8664e..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,30 +0,0 @@
-You want to contribute to _metadata-extractor_? Great!
-
-The easiest way to contribute is to provide test images for the [image database]
-(https://github.com/drewnoakes/metadata-extractor/wiki/ImageDatabase).
-
-There are several coding tasks available to be worked on in the [issues list]
-(https://github.com/drewnoakes/metadata-extractor/issues). If you have something
-else in mind, that's great too. If you want to have your pull request merged,
-it's probably best to discuss the idea on the mailing list first.
-
-There are a few simple but important guidelines for pull requests. Code not meeting
-these guidelines will need to be amended before being accepted.
-
-* **Keep your commits short and sweet.** Only change one thing at a time, and clearly
-  identify the change in the commit message. See the recent project history for an
-  idea of what this means.
-
-* **Keep your PRs short and sweet.** If you have several features you wish to contribute,
-  split them out into separate PRs.
-
-* **Match the existing code style.** This include things like brace placement, indentation
-  (spaces not tabs), and so on.
-  
-* **No 'churn'.** If your IDE changes lots of code automatically, turn that feature off or
-  use a more friendly IDE. If you think a wide-sweeping change should be applied to
-  the codebase, please discuss that on the mailing list and, if agreed, it will be
-  made in a single commit to all code.
-
-The goal of these guidelines is to make your contribution clearer to read and review for
-all, both now and in the future.
diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt
index abf4a4c..5715329 100644
--- a/LICENSE-2.0.txt
+++ b/LICENSE-2.0.txt
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2002-2015 Drew Noakes
+   Copyright 2002-2017 Drew Noakes
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 6975a75..780ebb7 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,35 @@
-![metadata-extractor logo](https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-500x123.png)
+![metadata-extractor logo](https://cdn.rawgit.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo.svg)
 
 [![metadata-extractor build status](https://api.travis-ci.org/drewnoakes/metadata-extractor.svg)](https://travis-ci.org/drewnoakes/metadata-extractor)
+[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.drewnoakes/metadata-extractor/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.drewnoakes/metadata-extractor)
+[![Issue Stats](http://issuestats.com/github/drewnoakes/metadata-extractor/badge/pr?style=flat)](http://issuestats.com/github/drewnoakes/metadata-extractor)
+[![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=TNXDJKCDV5Z2C&lc=GB&item_name=Drew%20Noakes&item_number=metadata%2dextractor&no_note=0&cn=Add%20a%20message%20%28optional%29%3a&no_shipping=1&currency_code=GBP&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted)
 
 _metadata-extractor_ is a straightforward Java library for reading metadata from image files.
 
-    Metadata metadata = ImageMetadataReader.readMetadata(imagePath);
+## Installation
 
-With that `metadata` object, you can [iterate or query](https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted#2._Query_Tag_s) the
+The easiest way is to install the library via its [Maven package](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.drewnoakes%22%20AND%20a%3A%22metadata-extractor%22).
+
+    <dependency>
+      <groupId>com.drewnoakes</groupId>
+      <artifactId>metadata-extractor</artifactId>
+      <version>2.10.0</version>
+    </dependency>
+
+Alternatively, download it from the [releases page](https://github.com/drewnoakes/metadata-extractor/releases).
+
+## Usage
+
+```java
+Metadata metadata = ImageMetadataReader.readMetadata(imagePath);
+```
+
+With that `Metadata` instance, you can [iterate or query](https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted#2-query-tags) the
 [various tag values](https://github.com/drewnoakes/metadata-extractor/wiki/SampleOutput) that were read from the image.
 
+## Features
+
 The library understands several formats of metadata, many of which may be present in a single image:
 
 * [Exif](http://en.wikipedia.org/wiki/Exchangeable_image_file_format)
@@ -17,23 +38,37 @@ The library understands several formats of metadata, many of which may be presen
 * [JFIF / JFXX](http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format)
 * [ICC Profiles](http://en.wikipedia.org/wiki/ICC_profile)
 * [Photoshop](http://en.wikipedia.org/wiki/Photoshop) fields
+* [WebP](http://en.wikipedia.org/wiki/WebP) properties
 * [PNG](http://en.wikipedia.org/wiki/Portable_Network_Graphics) properties
 * [BMP](http://en.wikipedia.org/wiki/BMP_file_format) properties
 * [GIF](http://en.wikipedia.org/wiki/Graphics_Interchange_Format) properties
+* [ICO](https://en.wikipedia.org/wiki/ICO_(file_format)) properties
+* [PCX](http://en.wikipedia.org/wiki/PCX) properties
 
 It will process files of type:
 
 * JPEG
 * TIFF
+* WebP
 * PSD
 * PNG
 * BMP
 * GIF
-* Camera Raw (NEF/CR2/ORF/ARW/RW2/...)
-
-Special camera-specific data is decoded for most cameras manufactured by:
+* ICO
+* PCX
+* Camera Raw
+  * NEF (Nikon)
+  * CR2 (Canon)
+  * ORF (Olympus)
+  * ARW (Sony)
+  * RW2 (Panasonic)
+  * RWL (Leica)
+  * SRW (Samsung)
+
+Camera-specific "makernote" data is decoded for cameras manufactured by:
 
 * Agfa
+* Apple
 * Canon
 * Casio
 * Epson
@@ -46,57 +81,44 @@ Special camera-specific data is decoded for most cameras manufactured by:
 * Olympus
 * Panasonic
 * Pentax
+* Reconyx
 * Sanyo
 * Sigma/Foveon
 * Sony
 
 Read [getting started](https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted) for an introduction to the basics of using this library.
 
-# Mailing Lists
-
-Three mailing lists exist:
+## Questions & Feedback
 
-* [metadata-extractor-announce](http://groups.google.com/group/metadata-extractor-announce) for read-only announcements of new releases
-* [metadata-extractor-dev](http://groups.google.com/group/metadata-extractor-dev) for discussion about development and notifications of changes to issues and source code
-* [metadata-extractor-changes](http://groups.google.com/group/metadata-extractor-changes) for automated emails when code, issues or the wiki are changed
+The quickest way to have your questions answered is via [Stack Overflow](http://stackoverflow.com/questions/tagged/metadata-extractor).
+Check whether your question has already been asked, and if not, ask a new one tagged with both `metadata-extractor` and `java`.
 
-# Credits
+Bugs and feature requests should be provided via the project's [issue tracker](https://github.com/drewnoakes/metadata-extractor/issues).
+Please attach sample images where possible as most issues cannot be investigated without an image.
 
-This library is developed by [Drew Noakes](https://drewnoakes.com/code/exif/).
-
-Thanks are due to the many [users](https://github.com/drewnoakes/metadata-extractor/wiki/UsedBy) who sent in suggestions, bug reports,
-[sample images](https://github.com/drewnoakes/metadata-extractor/wiki/ImageDatabase) from their cameras as well as encouragement.
-Wherever possible, they have been credited in the source code and commit logs.
+## Contributing
 
-# Feedback
+If you want to get your hands dirty, making a pull request is a great way to enhance the library.
+In general it's best to create an issue first that captures the problem you want to address.
+You can discuss your proposed solution in that issue.
+This gives others a chance to provide feedback before you spend your valuable time working on it.
 
-Have questions or ideas? Try the [mailing list](http://groups.google.com/group/metadata-extractor-dev).
+An easier way to help is to contribute to the [sample image file library](https://github.com/drewnoakes/metadata-extractor-images/wiki) used for research and testing.
 
-Found a bug or have a patch? Search the issue list and if it isn't already there, create it.
+## Credits
 
-# Contribute
-
-The easiest way to help is to contribute to the [sample image file library](https://github.com/drewnoakes/metadata-extractor/wiki/ImageDatabase)
-used for research and testing.
+This library is developed by [Drew Noakes](https://drewnoakes.com/code/exif/).
 
-If you want to get your hands dirty, clone this repository, enhance the library and let us know
-to pull from your clone. Ask around on the mailing list to avoid duplication of work.
+Thanks are due to the many [users](https://github.com/drewnoakes/metadata-extractor/wiki/UsedBy) who sent in suggestions, bug reports,
+[sample images](https://github.com/drewnoakes/metadata-extractor-images/wiki) from their cameras as well as encouragement.
+Wherever possible, they have been credited in the source code and commit logs.
 
-# License
+## Other languages
 
-Copyright 2002-2015 Drew Noakes
+- .NET  [metadata-extractor-dotnet](https://github.com/drewnoakes/metadata-extractor-dotnet) is a complete port to C#, maintained alongside this library
+- PHP [php-metadata-extractor](https://github.com/gomoob/php-metadata-extractor) wraps this Java project, making it available to users of PHP
 
-> Licensed under the Apache License, Version 2.0 (the "License");
-> you may not use this file except in compliance with the License.
-> You may obtain a copy of the License at
->
->     http://www.apache.org/licenses/LICENSE-2.0
->
-> Unless required by applicable law or agreed to in writing, software
-> distributed under the License is distributed on an "AS IS" BASIS,
-> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-> See the License for the specific language governing permissions and
-> limitations under the License.
+---
 
 More information about this project is available at:
 
diff --git a/Resources/favicon-16px.png b/Resources/favicon-16px.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8b96b53a0220c3da61e025a31b6351a1ea20bed
GIT binary patch
literal 590
zcmV-U0<ryxP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004b3#c}2nYxW
zd<bNS00009a7bBm00066000660fB>5@c;k-8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H10n155K~y-6jg!4=Q&AYkf6u+QktEbE>X6nB4kc0%oZM<M*g>gu
zb?^_6c99HX8U$NvI@IFi<m4v!v8a=pt00Jo6cnTg6)A%81DlX~HHr6iXj+=yVDe1w
zdEfK=&I9Kh(Lvd4_LLcyfl<&$$rjD5A2e-73Rrd=@}*Mgg?5`Wnam{UekbrQ{gpW3
zMEfeXiZW>4jMW{pw_poE1#r!{)&>|KKbJIPcynb%-+x+=?BdqkOjEw^Uy``7x#pM$
zFfx&wlv(i;3=hyKB<kf^V58-$Z2?@@+IkW(@9>%2$uP8gfB-77)I2bgsDIfu{!gA+
zI0uqJS;m6Uuk}5H!K-@u4*0zQGzuW&=E}pR>%E-X1^`<H9Yvf?pU%Ar39CCdY&1Ui
zCF|b;Al^BFoJE=40uGt%u4sQ6)!0xVM}@@iud|&14rY71q>NX*!K*dzK(X0u29X4U
z=~n$N3^nCy`}*3no*W8ZKfe7w0sv9~!XA8hwDi17nut7!Wi!T1IzKXTAylYpH&d;#
z_A^=k9sqIA6y`DK8ODH6p2+u#sv0{O2zMq?FVDF@R$i{{OCNuN=?JhNaC=DJ_s8uY
clQpjV14#v~Z7Kc>xBvhE07*qoM6N<$f^jbZ%m4rY

literal 0
HcmV?d00001

diff --git a/Resources/favicon-256.ico b/Resources/favicon-256.ico
new file mode 100644
index 0000000000000000000000000000000000000000..abcb5576c05ae2ddc411faa952304b99b7965202
GIT binary patch
literal 9979
zcmajFXH-+q7cP7f2py!02vStK5I~x=fI+EBM?g9V0#b$03B5=a=^dm6q(~J6A|*;k
z>Agsi8hS5p`2F7xcdh&3X05Db_L<pd_MX|#v*+vs03ZM#xcPtpR>1cz06fF}heH3?
z4u=3hJ5EMP`M)+k7ywk*0DzVCf9*0n0C;&H03y_1JSQh%B*6_OS5ky)-dw%;5)t5j
zUb_^(0f4)3CAgfnN5<BSw~vukCU$oKJE1gLqj1L{=9j`?(t`pospryANcv``i(M0*
zM@q&n2}Q97EaY=%P-L2?=bhkLTF^qq?c#i47%bIT;?dgdTkecIro-%m!HYmX8tJRA
zjO{XR-G4qgsp9@MVY4~AcJ!+G+BSXU&?nv9|Gdn42A@Vr*t)-rS=P_Th*B|==rD&I
zt;Cw3<Mx%P7VVKkKVI7X!Zba66Mv?pd!v6BTnX`zgV*89?P!XoZylff<SDTfl2C#d
zg0%wP%AvKXW(&HV*)>a1GyxV0#X1EqgP8_z*+c43Uq>X2)kp4)z#Y}1H}sp_w|Hf)
zRV^O2;H9+nW=S{kC-)S<G|~F#653Pj%S0n~DbQYVxJ)4`)~b5qT_;n1#*zO(KLH{J
zmG!9YedM+D6n|Sas_gb}RW-G~P!YS)O}-U|0!s*`W|yT4wNpfRMujqo__ygh+Qjhj
z&)ovruD8leTCcvjSTjV7j*h0uUhT%Ng8k|6dubMAJ;Q$!UQ`pMvO4uNH8o}J#O0dm
z0=a({($YkG-T6;G@mw>f{FD4ml?Czy0==szHs8#d0Ux_86H)1ZGwM)(z)TyGLkmk1
zI+xxf(*vUwCfC@OA*L=8R(!_0cV9Y}uQLw51aqGDYXOi(?PbE5nP}eJ`&<Yl&T|Sn
zGQ4dg9x2SxpLjrVbibPd&fC6@nnUI_TwV8vS?39KnpJ}t=RBXSLE>0OM`N1257$Sg
zRHR?=BzpN%?mVQD=|{6$(_Y^FcWi5C=l|X7aJ^Y3=oQE#PPyO>aIKOU3({%Xr|r@m
zG(=*pt0$y8XFYM6OTK0J(VavQm5-kv%O#E}EX7PtI)CWV$O@_gbLCG|Tfd|ToW}+t
zdH?Rko+zR&D3&nW+iu4~PQ_4;>NkB)T2QF<4LP(9Rn|lDRgpo+g#8f<SKG1ugEFA|
z>Bp`mI#w}|mQNUW0%vAI>F5{3<BI&#zAs<CM2O9L{CyJV2>wYcVW{|)gDB9h+q15d
zB&XwurFxdXZK9>xW012=O{-A#)ZN;8wME`vODNSI_(pirL;Z%dRAx{?L_}nc|K}mT
zE>Lj4%W_Wz<>DGuR338n=--@W^#o_wY*~AO=1ev*I4K;OEA_igDbbkfa>HKqd_yzF
z0-_GS{o3h=v-k1#rJ&*mEFaq+fwnQB)ZRyAE*ajlo~xM+jwb%rA^o5BCh)wBMAMtA
zweenoX!W&*Hm^CTrwPALA(Eh)%U8~SQ5E9vy{3jAdlxG1u<pEw{VlJOXcGP}v7mu(
ziX<l-=L8+rdhkd~b?~$!OrYycwE#y#*8uW@l3`U+JEi1Ly5g^Cw~f(e_1;SZ5UmnV
z;#nw<|AyrA;0ul5+VhGKXvpcURm*Lgfif}-iRtF&67E{{VD~vWRfX||jMt40GmRph
z<EeCLhMKy%(eJyM(QKsOFoIr7LVJQUFBum#=a@zTUZjRHM}iI>0^?kT<tt=g)?T)l
znb~;q^J~H@;}`=1@ZSB+cE<}P<W<?XZ>JYkn;j&kw797w0(gB_p^0k2X{>{UY3b>*
z&~;y0VfBZK1)6%LF6me%4AUYx)f+3J)U5ccXS_%%8(UikkS|v`VlR~|Vb#v=r?1WD
zdb%muCGf+9t^|bxhx46ur$0l8t;zk?=VwjS1>w|U=Xw`<0C`I_LqaT(@C(VUnlWRa
z-OeJCOX;IleUSFDBwC`yl|fiae{y{M#DsLV@sq|PYpSX-(N~gl<J55V0>pH~`YqZ$
zDNs6y%xtAMwU)Ksg2j)9jeU4)YV7(@zOEL`&~~!>w>4&F)Q(+gRZLX0#j)W_8L*ck
z?d_h4nwhs?FYT5<+->hQ{sQd%!`|9PZ)=Ffb5v`V49C?QPE~EOrlqABDjfLDNknl6
zj@jCxik1y2Ywn<b!-E6q)uSSiG)LQ8=dW7?CduZBaNChO&g87$OqHs+4uQ=&mbyCm
z+Z1~d*ayTMS<Qi>xWCe1qDz~7Dg|DFnDR>A9(l3~<NRDWu6@Xhi;m~b*=;2A&>!S2
zGz?lK?B6#I$Lg;R<69jdl_o-|Qfk4UaE|{J8E)*nRG|;Lm{=aqD~_ly#9%P(<@UAU
z@s~O>+mb<o4>=;>i<P87D4&PK2`eir)j_ZgpH;4pGI4W(N0B>`sBO!6tmjA6cNKVH
zP}a7j<*!YMCw<NB19B%xh2atxCMOh+b+}U=Hm)<Ly9kNBck+!6pv3n|#gbXn;CZMA
z`<IoM`@L_Nf+ch<5u>)Hs4nrH_!Cb}Q{JJb1@ba~GP(9z^dt++i40?KXw29^Dkn>v
zp;H)DFgQ3p?X%M8ws8g7Ou3CLEF5NGpWK;(-3Hf3A9cn@M@JWZc;G;};KGwbr0r=0
zcO=;EeN!#@tU4BFhHwyTgJ7ehe-hTRdcy&q_XdISez~@|Mzsyt*`lV#%JlcW1l^OA
z3uGAZrysTzvj{S~ka%(KB$_u{i6tr<<#aQ{H<O%o?rm)MXW;1q8a}<p4pcJPU6y_Z
zo6G~L!q(H<Gv4%|F<S4rtamgEL#e7O*^$kEErmS!8oXfw+(#`c#T6Bfb4$^8#aeWB
ztwGukl~=FoOBzX)cNYj>HG3V-+KcW(s8;X{A_b*9MB<&)3Ql(LaIo}l!v1do`?Nvi
zWwN3hj<NVEhmhd31LizR)Fw6C%*;$}?yv<enmTh#_zuObDn*D~s+$@cMHAgqVptQp
ze2ACM3z&;Mv_&9v#sJQz1kg@I47fPlk3FHwo^K3^!(UMy2Z1q<hdA`?+!(76m^ynQ
zIyFvF#y93+abKx;2MoM9p-^Wyl@m?(EuPMQkpgXDUea5>SI+=%sLBYD$hoihV0szN
z(k;1NtV;Mfj}!)9_qFCiw~h^K`}X}rm+Do_^5$hjTx2jTk90&!*3DTg5N>Yo=oSoE
zEMf;ca8A-xxDBJEJOfiMgvw`Ojd^hH<DR!}!LX38p8S_Bi1=|NcvAj561gIJto<Go
z9^3-dsiq1o%gnJ+6CWGtfafGg4s19Qh<}8=Y~7NS1FiFZ1K^2qCNu&FcXxNMY(Lx`
z4<jr45i#=NPGMLw0B1LqX-CBdKc`Mf={Q*Y`eHx!_QJasS2DTMQkv^|$&T!|v=n&m
zs%ei)Ha`@chEwLXw&q7bYt2b>Dk`uZb9X{HT8(b9krpkcxSr+FlYVvN^RT9pq6}@3
z&ADoBIzxb6XaW(M3$c<35vd`+EBc+}`@H1O!2^vs;+UNWPQwWvPc2<K>0$gor+lp$
zFmL8SG4$K%h48jSf%Lwk(!K9krl;a{=A^I(MCW#`#(T{3HjHhmVZ=r<@#_8q7(IIs
z<=*j?#A@AnztaOM`Q$bAZ?E}-Pag!QQZVXOLE;o2Wa*M)Wo>k3HYD3wk4`9{pKzE&
z={NA8*A5uL&CkflC_N+p&Pf86kD~?tc(`5%rxT5~Dre6KOGK8a(-{G#4^4RtH?v4C
zW<Qks{=8R`ws{uRB`&59{(>pNm)P?48~I~$>&P3&gIXop>ry(SV1JF3Ul<RXKJdSP
zPD9z_&d**1-pLzgdkh@Hj7-pi#I$gj+k8nFk2vSl-9KYS4W6BUeiBX<;O+;H=SDVq
zzQZ~<xatJn+Fv?afr%@bK8O|ZO<x?%l0jTeuj)NAkR2z`1sDDa{sw$DGd@?PJEhIL
zo&Q}+19QUqH==*dC7qZ-nDspbdboI`g*rPG&(q_{kl7JG&Z96B23Z`D_UR+nZuiMn
z$jprDB>sIEoDNg=thfqDMfdvL9iISc5&PUmKK?vqu>NN&mN1|k^tRAPfL8)aW7At7
zkcf<sbcCqORy5bj%(N)QTY>%8xI_E!RTX%>L8>oJ{Jne|FU^9_1CEotFXYBm(b{ES
z>CjzY-qn(?9mOE0htQB^#(Ee{=8(>f4W9~t_0>8fap!ueq8+t1@4My96e*Eff53_y
zk|@SMTg>A6#2E(NRgoPOgsKYM`XJTjhRaWSobLuUuTAETCqm;Z+d*1_%6mWT73*uY
zmzVU=VJcPELN`*XNMnF}txkYzkJ9E>cB3SpBonksuNNE_`rbQ?y`J8)Rc2xV8Bx3F
zqi$$O<M!yC?4<v97BO8gM;+!hAZ#FMyN<}*%PICLX9$q*9>rJXi~a&UdGh+Yy#DIm
z!Xo=Jv9KWxv`V-TT<pKs*BO2lYaS8-+}uQH9bBwZ?@zNdX&xFu#0#Rio6my`O*$&Y
ze-fGTP}yD$-O(0Kn!`WiUdSWMrH~R{B?6-Hyw8I??rT~QfG^AB6v938FgTMxM}Gz^
zS<biR#J^~pv3Dz@;krK{gM)u$TIF9HM~C88<8vShPSKTL2sY`7YR5!dPDF-02@K!i
zc?rK~+P~@$IU4SEdYDnq1#dugPuxTPO(!%Q69<9*4XpbMA1S=LZ~f4e-Ru|mmrjow
zde^F(`o;kVd-Ui#kKPH>mh)MsFnq-+vGpWnE!rE&ZE+xZLmI?ULvV>4>$CH|rLKfH
zO-#crnmS_*0AJz_-s$^6g;OcNS`1DRUG3q-OZNPwNld~6(h(bwIM$U2O6`8H4SIk&
zYemj%YALt#ler@h(z7EYwIvz3s#Z_F<6O*y%HH`@@R6(uYNIN_qzjP0Z1&{;)z#9b
zonm&2Fp9gjodg#D@{~j~w+uf@k5KQ-y2e<##9xeM_fDZAtp~x9hG{j402RI@e1c<f
z7}4;f0d>XPBAn9wz7jY?M7QFb<qbeBaw`=Eo18{uvHI@&ye<0-G!TE8F##``@Sj}y
zM<2ueYOOD@*!*hYRmmtp4VQnNGTe^b&z{TqLmf;O?b1j2rq^yRXhWdVzL#sDZ(Lul
zu-fr2kN8b4;;^h3xIP_Hxja>!KbG7!ow9dJzvaMrPA)3q31Lpa<8unloEenY?-jbE
z1+hW~f^s}c=^odZKrWva#PUaST;@h|dwXfL&S0K)PL5<}f0pip|GH$bpz|WfX3~?k
ztNB|CYGb!wV<kfco&UmXW)b1w>AmS|qcaqi64Md=v}B*zoI&!4BXG(m49XG1#Gac4
zD&9kXxb<jeMKVm#c}bo85CY5d%EppmIuh|x7bW4w@^cMW!I?+Gg>WXP3@qho_?}@?
z|6Ix+=Y<x?D=!~TC`Uh2xA+{}-XHXf%R5@~6m92|j&9~Jy9En_nJRk_;Xbr#C;g_X
z84|%eR|2JbqH|PH5yC~vHhL3~*=1a6<-8IMWwG9PKSzv{6EZG><K7Cd82bleX>oI)
z@n}bcvniUeiw_`(EEc;<_5!h#PpQ21as?m$r6hL)0jn=?=+by&U%Zyp2P|c~BL0%Y
z4s%<IxXU87SvCmpX}k%j{~WM!=lV|%<*Xg($Te_U@Lm7ycJH}BL8_&Ho4sroAF!;D
z6PQk(SNu)iF7yymN7SodgEGX9aizBJII}&afq@Ij8V&~m#lIf8tsS}J64(el!om8A
z6=&9+i~@D`%3{CSiz$1xoXtk<cJJfjI}(IDP0w47Id<#@^q)ODScm7WT#pRAJCDP1
zCElbN_z1S(6ADdX_OS0%ivDW?7@IZ!A6>FzwY}HQ?bAn<t#hpOjZICW#f{%y5qGrx
z0INISIw2Dxc%dzz_tVvFe{$?`O^v_T8Dy+^2vICvO&xwq;(+sT?Wic=*sELfbsH&4
zYIXi0ojc_Y=UjZ=vj?wahfBxP)7}hF6YGK!x*2#YP^gZQSgE~VJuR+GJc-IP1>M%Z
ztG|lY2eY}PSe>~6D->Fr`DB+$$C@O!!3bye2QNLoZN|-wnHi(IX*f;fTEr>w5}RMp
zi!R4PJI;axrh(|MXP#RnIM!Hz)iCP<(mgG6cx8v5I)jJsgXwT8CQd~wsl)jZO9J6e
zrLn9W!yH6>xVdNNlbG)Bb@FaSOTD4g1q<Q;0a{7m^!!^;iXyIm>JJj&^`b-DWf|;m
z>?1PFuN=r=_2x=FeA#fB!h*e>p1{I<=MDhft*!U_9DL<$hGCIp3J>@W*%{VC9D>A>
z1V2RF;|&P!F$92SUi!IPQW}MP9BKAytZkuu0E|xN!=PT$V-6HfkQd4e;LN;=#@|CM
z6IrnLQ4q4*jDrDjvsJ-4@KtSxzUqNKF<ey+64&Q`!28-P3|t84f~hpsaalqxz~ioq
zrfs7qxSux<2L9<+{iB!Lex$=d!wY#t7TEto8J$i=2jocF2Js?`l&LTs{gq*OxU@@y
z7v%rU0Jvyz?Q*#Nt6;ntTnN<#vxzQ(0S||fyku?9&*NsQ_T)_*rVr<T7L(+d8BZ;<
z`;_5?Woocj8gpNBB<65Az_QpP*hbMrOiN*#Pl18Pn`Y4TTO!OOsu+0F+kA!J=>|Sh
z<`*`=sn+xW)zopE8EwnWfBw@cNN1jdk7J>xK++t)F>X?1wMt5-HoYMYQ2>rjuJY_Q
zs9N;bp@oa%3rOqs+py<h_Z2T#a_SD8RUu!gcW@4}@mTB=oo~1LWzLiQh1f&L5~P~G
z-}zT(>FzR#1y34I1hoV;zz@Zc^@-dc1l`5e-*}Sdv{?OUg2?FgLoucg)pbv;fq+^3
z;ZFjQf`t0SpSp4&>d6lNvUy){i`>h`k@I6O^dQi(g}e+O<1h%OTrz-lXLP3AS5ps6
z6s6(H%qPhyx4NQLZyFj1R>%#*dxUDN&-k%8Uks)^;4%Fk#)nzbQY5y-odmA(GjbQ_
zEC0(W)GxnRKrv)y<J8dmh*zNRqHBE;teod07QN6=BZkNKU0;E?#=BdIo6K$#s4bu<
zE=-@<Jy*Nl-g+d{PTTb&m>iuCPRCnXwk5tAPQJgx+)EoPUjGvjDlYyC@NKHk=5;E=
zQOg0>uUPj{g4Pq?)sHhR?Ko;7B!$Jbf)7)zwDJ8yY^GM0RT_P*1vzXfXpvk`#5DD;
zh;S#1hHpp93h&)le0~aYM52JgqKa(#IPWklG$i4R;`U<JvWnQ*3v<;^I-ugE{)pa9
z6OWs-PPR$ifyA7~f7H?~(Go;!b9a&7$FIQmB$_)E&koS=SW4#cQow!<N%tWX<lvu<
z8ZG4n9kv9Kz1-e3s#;2J(PE(CEO$IUDG9zFof|vWU2a*tA)CJ$&^|=?0+$cAGc3tr
z{{|SHQ9>h#a5MYT@)_6U|6wt)yb-|TbuFA(KQ&|=#SP-*DLSPP{EU2Yga5pod5*`-
z=0xW)&XO$qIIwPTK?(iFaRq*sr|qdxlg}!YqnxSx0ghHQ{vBriND+5c3@3$o%peFX
z7JEUt()h3~eJtkF$NL>GK~x2k|G(HbHIT?m=Bb(LQb;2Jo86KOz_AN#_JEn4MPXp)
zKQ%dqzF`b)5ko7lS=Ln&qa%dE_Cj%tQlfk?u#4-^;39s7Gmeco`+)hTRg60v2-50E
zl(PBGsNUfH{V?TXQUInjK7_;>^*oQ~00K;ECOc$rNQU5C<CQdXK~gz`h+81K*r`z|
z|FG9Y8cm!Ta12IQ<Cb84=I_~z^!xV>3t}P|+BOhT*)j8HSn{y+x6AXEr#N=UyG|!a
z+s()~1)CISi(QM}<m~Pgrmy|Y@hS&gliDJTXLg3udl+S0`Yds4dk6E9^?QA-BvQ?e
zejD!DhDeE5PxPD=+ZH2#w3U3;Z>}nns`@71)djSZgyjoz4!f|yJxa)T`^a-^xM->x
zM<D%&%$SrPWrp?Jf~!mN@~^;zCd9h3^;=KHBZm1*DVurD_VK^2FgrGHb=lYD&mmYX
zC~O=YWF(0W%;p*9^eMzQGm$vHu?|?v`bs->FNbhhA()w`aR|Xm<IjFJlx3wNwsP(@
zu8fN_n-sC@Ql2wr6SJs@fXqM@N8>7Jx?>PD5p7oV2Tbe-UXrpB<e=c-`4RcQ2yd(5
zaC!wgD1Ws_IOuRQD&p5CH0$l5hNd2t_LT9|V*H*BgB5@AIRZ1T09@P`9W8s))twMo
zsP?K-kA$^L(TIBy3{IrdypP9)eTPQzEkc6kEScnuR>G5~snrZLdby7R<Va0>)(ww&
z`1q|UEzJ2pJ&Ze!Y1}=Z&P3ZSlbEYQh-TVMXTjq&Zx&v}L6(WkR3XG8Z8>E1a=wjU
zNI!oYmWwKZ<~Eo+jfdGPvR)^sx5^HtD3Wxhp(`Pko+FFK!36~*rmB#AV=zQo*KXSQ
z_z}_dIbKu*YXxZ#gbrOOLW)qPtN}NDU`8|ibu_D-C$w`gu3dUdH5n<<Ah?nfNI?is
zc}k^XI$jMtN4>1SCOd&wff+0)gf}Mww0YcOcuKk3CIYBr4J4B{ni&5vZ=f+La^z*i
zy>{t|jzOv%)m5*A1E_AJlSb+t^hPAcbpkP;x*t&KOAjx=s+3K&1;+jveOp3M|2sw3
zRgpPFb=l=nmlbD^Wp&rf2`ZZoJC03!x9u7VJSW1tHTM`pI|mQ`n1oI-uBi3wY~$;;
zf<com87%hFkd<ZHc3=y?R^x&0&P*N`Zc!R&p*Bavul9lz;;56yrZVJf5T(R<womo9
z?Xoc`?VIvS$7QcGj;&hmODnvp0RuKEQ%5pxdr&8H!DG89iQ?ji>ce{WjyP|u@f4}$
z4BU2Y#iRs9Hgc#B;~4SH#K%XtXO*bE*v{E{K?1JDG?>OMP>go6L#ygMg{eIEPr*X@
z=u@Yh&kz8*wRmdXkTEL&%^$Y7qZNSB1}n(oi~mXTH#pcl@!jT|e-qrI8EdT?a%U(F
z)@2QZ*``bx7sVI`?Dt5N3(}ct3kQ6qM+XRQ4DHKOG*XKHXg|Spj8N!gPBpo7h5FoO
z(g(|s0LW{ftH20vy8~fLK0snZkHW(I4O|%H2ysG3jmD(4%uLDh{EKESI`6koC8K=}
zo+X_}-tyaV9dXR`=zXqmsBB7a^(yBv2PRaS$;LKKn)flpsrapHi;oL?d=u?~Bl7rW
zB%EB5hR6!Ugu2OV=M7-#0m1=1bE#1gactJ$Tmj_MKAUDQ&AIK}=<vrMZ0|b}Kz}3n
zE#&zfiQy=KfKAw-T)IHO$xVD6#t2X4$xFaX>;fkoJ0kdgUsp2<(+4G?uL(;ZC+}}s
z*c#!=YJif$3wXJ_Y2g1|d;@UhI{^A$@l6kW8U_H6mp8>Xn}EJFLwju<+R)b8)tYvz
zNZNO~kZ&Xf@N`q98fr7-tG6~0ub1)^g5>g*lyc<deHMwM1NJa@5gj(_?35oD-MHK$
z&DDNU<~?9H#e)|UX-~f8eblQjQxjsonz5!DMqwg!%XM_p?%Mw-)%D0K_<^8xe}<1m
zujaf$oD$0N!(f?me)v7DL+!B<<}aWcL{nZ~Uhdl3)8xDP59`!&i6oF3%U{DYbP5bQ
zWN66<DxPoXePs3dC^#xQn0Q%?AbIZ!Whe{y@@DpXBZ<x|-m{-JV<_dsvGgM|R!W-<
zW`;OW>s%|PCCiSY|5UO02b)9`ZMZs<N$ei)_=lh$^C3-oi@Z*;-&|J(sD57}Ie#+}
zyU-m>+@m4B^QzKRPVRv@3!zV!_Na;WY3GKTH$Np&O)8uO&VIzN`QRQbIIzE8GMz1e
zYUlj2fHf>ZfY-r5j#GcPK4Fi6m}(wL#Hn-W9G3{wmHm)SzW{YoNi;XDQz^gY!Cihc
zBndT5X%_rcIZROVwV|PR*{Zy=v$MPPYNn%{HHPb2LwtJg1s84O%%#yve`dWx@VkDs
z1E<%mpq{TTMjd!xo9jK0FxH)V!7|s<((c5V7{d+k2*Sa}MyxFG1>x|v@TV2JZcEU?
znM#^^%j?~{Ev5O;e6sf4v$He*{9tXkZHD*obV_SLTU-5~wCd+E^i@B-YX53`pRWzT
zJv!N>*Ed?y?a=wZ!F7EiMbGLUjlqYxrKW$QhZWN4?^KP|$VY@p-Uk>fDOB$rS$cY(
zzkRJr#^X!2^k`Na)-fA0u8{?N{r<yZ7n<5IfUfxytx~!5lCnN|xZP^8JYdc0?gyP^
z(!3~T6HCt^PBj-x`ETo;pVBIUy>3x8%}u|^)}F7=ObTDbSbl#0pb@|HQpQ;|0{h$g
z0Rgk!dLH>HIubPeYV=>vaE9(^v9Y45tq_zCx2=ZVG{*wIGK=#lsvOWRd=#0SQr~yp
z529QP4f#6w@LttBZkgl57z=(0?oNP)DW8Xs<<0J(73-8BPcXqtWpf7BO<L{W&OyZw
z*~pwGxVgGfoXVre2*HokQEj*Qclmk}j*R>!LW>9|<B2#44DK@~{Zy=dXBVOs*ruvO
z)6M(E9_+v|{<67F^h=P++niGBGAg=_`gP{#d&;~|VP;X&8Z+7OlncFQE58;#SNtN%
z=1k=vL!ul&o0fFZp(s8Q;R`K>);e!70d59lwaHeQMto)$&w6k%U-QTKZ?`cObPM~I
z!<z;)jx)CK!xBr{D1}(!5n{&&?eMYpPlh!?yACxRFJdF6p%sy@;mpb3s|0B-4Z{G#
zkA>yg2Prmhcfa7B3jKbf>-Fr!>J!uPtPQ@O_CYpW(k1T^V*O%p0o6fnYNr&%{73rT
z)0)MBOUeALsn1L*aO-cIyIDsfbuE|PHplOZg0(GROo%D%nkNU8AHvFi7VndP{v+X`
zN!^YVeF9PN40ynVFQZ77A{?V(c69Kk`_bpYV8O`+`sB9-iH;<j?gIA@^O$~EZSBCI
zlM)=$H7}PI@9F41SE-_u2)3mww>CETEdLwt<JzA`kI$YZuGDDGUShT_+FbwI`+ASo
zunWwyybrjvcs4F+`RrNPv0MBKH`>=#^cHNpV}g@CDT(U5L+T;X$GJ3_$8%#7W5I;;
z>nyS}{b{?sVzxcDN(;Mh$#7ureEjM1EH&Hl%?nSzY{5Df^WJF9=h;(hEv>|lGi7Im
z+!r^S>z+H}RU;2R{d&J}U7zrG-`8NGDK@te9w#&?Wy%D#-}#JL%Bk3$wz~#RUi_US
z;eT8F|G;6~YZCzFe>lvte{F_)BckU9hsC3#6PoR`G#D`c1I_jE4~F^I<3=ec9%@o$
zOxp^L%ay!$+qo6z9#3Y@h;L~7WO#9DX^G>csyHF7lr=n@mi99d9e3a^UV5CDNW`(5
zjX~P1>7FXqb8i2lA8(cEw;b-s`sq$`fX3kSF8aPQmdEd&k-Tze+AGjp3vo{uPVIg0
z&yRD5|1wd00e;R?7b#>j8#TgQU2jSXgE;jyj1n-g$R<Cw@Nl(KnDaM?+gu!-R@G|3
zntW`i`nHln2o;J?|D?Z5Lw0MB>%sc6zG?n)={wNtu}Lp#LF`WIbQiS78wv1K4NE~2
ziv1@<uU=;9ixYm7)5io3RvbzP{V4T1qc~{>9+R;CUFmKw*l5m}BmfK{%nBK5+5OGL
z#HEsA_K#(bA8yQnS!X2zG89s0z!a&Q<u?9|Bjf6<&qF$}71fyl#T)OY&ry2cf0T+~
zVO10M1o8NRYr2>GWFpJ9=Vy~OSxh9xvMrY~U_cXSyXWIkbMRdyEnF(XN*JU1L{`X|
zO}p4uOzTV4k+A;tJ-~T@c13XY4hE^7_3Jni#VE-@gy7H+82BcuYh1pi!r~veMpVh-
z<gGY+{Bw&XL8SZwPYLqn9rqP!!~bIsu;Q+>{<jBQy}S+p0MGSi54;L#PLj4a)L{tC
z#-0gOv8fH$)PhPP)?v{dq1tuhG4Nbjc@nj^#o7;^sMBiufdlPl85kJakd#J%Fg`wB
zkV42uN$7hbvK(sSB6fvhr4R#=7YP_=9~pw5!!6D3o1^_rna4h>-r47_{mt?;jEszv
z@1pK#EsH~elYmlISp)inp`jr?fI6asO-F)|k&qq5_hE<dUp*Q!RX5fnUGRGc&c2(+
za~o+SqAV(>#d?XJc=ClF_S}9d>m7rs6I6mHZ|x0oLn%>&in->UxBFBmu2$D7_=NC%
z9s75A(i}m%CLW9TdCwZa>E!rywkN*qd+VYAHz|XAsv;ry%6KqLj>DF(|HBTo4P_0>
z$%K9migR5i&_n7Tfqt2BN1tis-*{DygjU1k6wqR#8@p<MZ*PV`EUpX-vUopBwOet{
ztXNPszEIDOc*1fRXvrL1ow#7A-YQ|paKBYAroQeUD!**ZP$H7~&YUBhGo_FGZg%Y|
zy>~UHh%@N!hqqK&(~-jcP%4pra(46K<v$sA%h!@lJ0~Z_RqrVlL`(-pFJSzYj<$U)
z668Xwe|7?JnVr-VnKG2g!dYE1tFG@t@)XES+U+3d9^K`4vcKg`l&CKuI}Vk*1cQHt
z5Hu&H;J`%K(xv>bYcCZLrLoiQP^bCArq3H8tk9UIK9NRP(QIJWT*MAFa?#bJbD4j8
z6YfC_ls}Pjbei$1PUT=<tXsYN@%90`Vi_uTJ?tfu_jzUH;fEYIS@Aqn{r*@h5nWOi
zUi3(*8-$C#IpN+*=Q4b)kth79-7QA0HWyYOFat4NQi}t9TiSrndkEV|FMAPwUQfV|
z*s<%YL<*Lt*FnH1HkYfN<5K2$y|jQG3MS8EO7r#0c9EyQaam?MkGs%hrU{uSKC5~E
zw-Zk~nd*6=!c(z}y4vGhlcFJ65hC=wboswjS^pCc)WrOGJOqwv=Z5RL`_|JeSRoU6
zp6@<#oAa_K1RZ=G8dlrr`L~Z{ws|@`_u}b2*`xjxG^Yst64G2HFT}rbG`U~=7Vp}J
zq9@?oX(o9SmB4l7YNOK)PF}V$h-7WdI#K1QAJW?&D+P#sO)futmi>)rk53+v&gQ<_
zoXe)N{AsR?!mIlzdb7Yz`gf5mU2H8EC72kT>n`sRKXeV+`zZCQAAii||BTiD4}A;a
Ar~m)}

literal 0
HcmV?d00001

diff --git a/Resources/favicon-256px.png b/Resources/favicon-256px.png
new file mode 100644
index 0000000000000000000000000000000000000000..660a6dd892c2c4ba4c49eb49be413cd72a72f0a6
GIT binary patch
literal 7111
zcmXYW2UHVX*YzY2sx+x8JRl0v38EAc2pAMFC?$01RZyxDI-y8Wn$mld7LX!UkPty4
z9h5F5fE1+#=}q{D_xrO})?`lZoVjP;oW1v*L}@>Iz(9AI4gdfK4Rxe006@Tx5P*gX
zJUsCzu?G*7o~jyX8t~;$V;c#c)4Hh}djbF-;`|MnuNA8XgO^^Y8NJYRwSR%J@~{Ii
z7>vYI7iUi!D>pj{R}Y7bjk}it;0jU$se<;-T%Y#yH?zwk{~aJtX#A{IyJQmEsWzB=
zz0mjW14TG2V=c?WxtYQ{HS;eWV~IE7)%%Xn*Xce!m%?V)Aaj|VB?Zz5M4Gw0_@9}l
z!kL#WhxrG?=79nNHo#bu+-2Un{ov=%hh85O)>?AvMo(MLoH9ms{WH9Rj>{dUDOok7
z9s0|8lmq?EnANjrc5@jB8hi=*FF(@M5xjFb#>=`NS!P78QBIe3ulE0iR>8bgkoA-*
zo4PU?>-z`aL`rRM%4;BtpnAbiRS0O7nZj;&e%&%0Yp{)4iGJbZFz!L7XRvzQ#}RpR
z?UDaRkgi%WtH#Y<>!M0$C>sGhMQU4bwqmnHN>3p|mtagNWjiE4PBP=a3)w1(R4T&7
z+0{%u@8B-T+zT4$r^3YIvc=n8ygpN$lxVBLm2-YaX=(L^-*z5d6I<dcw1qM2cG-Gd
zbc>42tkk5F`!sb4O^Y0V-z|yuWGc64J^ke2z!fz*I-0J0`ZsPF8pKZ7%Q~m*6ZxI`
zq=qJq&#kAqxjB0?KF`ts$onyuo-WhtC2^1_a>kSTTj4uPHpB-A@vE6wvnN>r@49T0
za2dZc>v2Dz+^Y&ha|`nN|NKU#21Y9_&dB&7?k+k$%F9=-Ja(^Gxjgt7dgHKP4}dkH
z7pbSGV?^_=31P4xa%%cYqHQDIsXQ@>B9M6Y@6Ex-Z68OiVY51(p4-EGvsC&msIbc<
zpZkAc@w}s>vCV$FD<hK+6^%rae1n)b1z42&3H%Oh|E~PrcXD<P`s};A(xMb<1o4j7
zEVKvC9wx;>^zqwlT?T`uSh7RSgkr~x516_1Q)VFhK{QRp`0>6<(wN#p?9ZR>Z+djH
zL#v@e1rs$6j~PQ|$st(LUt4hp>bMie1>(lW%l(^fC2+wS`@TDP9ByS*g`m%pEx@pR
zdk{A9Y>!u{ZU5Qza-jR(yRHRxK3RyKe}r(tjjV*S(GRBkl?8`=A3l7DlAZDXbtm2x
z`i)KARQ;(SO^9>1Pkjen?$<runi+|<34D$B;Egsdy&}}1mxIGHUNuPXX4*606ZJvQ
zMSJ=(r9rjZw{MdqzU@*P0EO4OY_}fbJUpX|D_$Lm|0dbiOx%c=DQ_>-oz9_!CP%{a
z?tX96NHS;nxB5)xcvUyn2Br<=eB$=S-EV*6pOpF=ve?xhl5Jx*(|W}(cx3v`_$+5N
zx>^J|yy{QfnxODClgVhQK~oq(*o^gt*3JYkPEmiJ#3aLY7f;=P;VR``_)ZSr@+(r`
z<lB4{_g(d2l11e2q{2qANxIxf5Cna`2IvUBCTz+TA=zbLBPp2BHGn-~=2})jr<VT6
zQ16_2xjNdS-TTi3!logTbQCTUw5sqR>_jK5?zr+5{M8}Tvh9ZBK=}nCo#op5QsFx7
zFfWn{N^N{D^GTD-bkpsQ@icY<S8aX$=;y!0(Hv~xFor`<9zAg*KLzZXV`7t}C{{;P
zFhQRJLv*huixu%Nq8DwZr&sTM`<P&49&17c-MY5c?s}quJuUzA>F}g_?JJ!n8#r}L
zuxP+CJV`4ooo|pjJtIRIz7oJDtu3HlsB2i}kwNAra?dlc*xTJq%YJ)$UlhyY=;Y)A
z2@tBlY^4b$EIT`Y3vhhjz&@$G02N5+N>JNzIo`~0`!R%B|GC}z;HddxVdO>GW5W|e
zfB|2_l@LcG{YYWGcFf%WZ$~lRKgGRPV+eXtfgq3f<dVK?{BwN#z=D3JDN$#hFAZf*
z^O5e@JS|eY5Hr=d!o;?97m@+FV71hnR>#+1!yCwYm4A4Ba_nqZwZ0C@)pqdrS8MF_
zs58IDvaE~@-nH>VIk1(g=;xJ%o1V4dFYA`aTxst${{U?LCNpgiHgsg)3fAbB4#zi`
zPFAn;rKhKxs_g`l<fDZ{#+;mR#fzrQwU-FrkzpYm+R;&1*1e7O<0p8@pBH9nz@OvA
zc#ETcD-=shJ%*gUfA{p@S99D+NFNZpXGelW3x8xK#+0@AS4kQ{xbsV&?)mUZgE$wB
zqjyEY?s(9W(?&N7|H6Q0<<g_$|Gc_8)^NJ}w$%kzWpOj@u2xtg2z=t}NOSjvN@K{$
z#Nv2<NmN4-kw|Q>cvc4;f2^;xp%5x1AQ*+5uc8md`3uk{EG;e7gd$e`mxbOb#gjtz
zUT?m}ZP?C|ecs_dKSUOVW^X9icCNvEIBGd}7~B-phD$xT-Eblfk#70q_>SD}Vl4T;
zgHP-LGo^1D`GQR?g|~KCP<cf~;ETpdL_*gBEpFp3%RfptiKIiz)aSS<$^5Ku+@8HQ
zJt>l;+rvbVjW0LSt0>cE>K8>94h~LD`7brSTs?)YrE+46iiUale{N19IH3(OdmV3M
zVq%KlTz6rf^AO3ULHn2?U8y#D?Q0b7*TjKkNQd$@N;SC#C6jGyR$VAX&l#Aa(-ZF*
z-8SItgqs{IH{SM@@=De$RN|tX5@;*omE!TB^S!Z|WZhyXo1|%$+s%VqOLo`)Z*`+T
zlfnSd@$cPtVNuHIvJEs@;~7Ykc9`0j_TzwzvH6j*pR>*lrJ<H`Ubp<Rz3C&?=!cLL
z-ormEsjPG*EyP@r#q0ldfS?65mrol?o9H$F&QTk+`0ma;li7x`EK!)emb&YG`>mT+
z;lU;aNK4Nrp8YE1pE7y<FGc+&s90iELs)3~4o^NaZtddL>FMdZykQ%#n>unW#CGNE
zs&3PG)HF9W$s~ED#_}a}`O_{O7xEN)qi@65%>fWoDtHGb7FrS+$e+;lOl%Ab@~@1(
zi{zNM0LY#lt7DatlShwaCdV1e#m2mCu4$BPLIL{&Ms2P`6`72HlBt46sc<~cg5lzS
z4P3~^>daU2x!2UMPc0I7yA?J{P}J}9=@H1400$vL>)0?lpzj-@%&>AsG(QLCp+w{r
z*Ow_>vF5eGynOkbea?i}CT{R62$HTECxV&z2+BMcu9{6Y7XjfDo^`+zc^Q@u{^u5M
z2MQ+mF#q1WeM)l+{Q?phh6n0VX*U;@NLMe??wjdDN%C|%j)DoaUm_m2t}CcORzyDm
z$fS4+R!NMPmzQr&Ab7{!*H!(PSk*|k2=WC0teeI1YvroM4gJ!xaj4wc$#xv)+;hC=
z1(mWg*0WiKuQ^ZI7%99^>9<PP-V`22GUvCp7DU18tm$(rE6LuZOW}g8X6J4Mug4wV
zvp9OtuZ?{W(Og=biN3u?su5;41^91HV8ZiYc1o{q*D_p@`AqkDR^i*=jt+@7cJsR1
zaDw+eTaOM7gv7VW00%CjJqZ%av5`@PY)g{N=-VsX`b_4&Cs%JxkGM{A?A&U;#WU-8
zxeXOTYo_#8J7|Dt_zc3lwSOwVTz}l}cAZ5v<&XBKClX<Y*Td2nFB?|F;?=Kb8!(WS
z9rdSI725gs4jAEyASW>g4qWeb21da7nVFeoWENaHNFa*|;*qz88}z}P1cF@!e`Z7y
zwp5$l46qci6fs@PraPH=QxWj(ztZ%zqtGrnS!3u2VkxEkdO+aF4-2L}KTrqtO3^EK
z^+%yWI!m2IZ`MBO@851iIpgm4AB9}XAHI4E*hQFG5Tt0?kcgMFr4b@>Hzu$A7&B}1
z>G<)Dda@9_AM&j*w#nx?*}c(IKZI#}VQ&c`r(t<L?sh=N{Ajik=5%V=P~1d$oXP-N
z^dsyO@ZQS&7{z|bmd{!6Sx<*}!1pVv|BpuoEtfRk3mANNeoqf~bSRf^D3YnPDYu``
zXeAA?*`x0>#{S{#Q?68+9@S6!B@mW@(DbQ14NfEU`d=BJfauZsb7F73pEOzdu^vYq
zTmgAn<S!{I4`+4kZ3s@nMk%<$w3RDc>XfGO8gK2OL4Sn9`zTRrqJ9w6V~Zf)fTn*|
zVaLJy$$lp)<0t~U{3AP|>%;RphCh3;*r_1`Z1Hjff;DSM|Gb98LSSXN-b~KD;coG!
zR-50I3LeJP*Lpvo>VgWm%Rkn6<cw)EO}eXJe`TabsR<iHP%VxB5)E%WA6PrHAni|t
zzpZMA=t*gAeR-zdP={VzFeF4gtUkMW9%XrL4lw+wmlWD!cI?b)QV>(%hF2T*LgT|<
z_(kwHaQL(;P0V4V>gN5mO-)%}ia%HW8T6S~)&MG4PkaJMn<zM~V6wJyOZ+Rif>pam
zDN$lEAAmb|o}5)Qoc=dA&%a13ZORI-mM(&p1a0+oM4rZ3zls9RZ*p@5TJo?Vh;`wo
zb$AqwD1`M&fe1D{d9RG}&g)D8&1YJKuWga^xf0V}MIy>VYN?Ucw;`-P*F>=Uea&-{
z$VH{xBBW0~5iI$A%zMC=_jp4^?gQG2zgv@lH24A=9Q>iws`_X@CLCOi_aSs(qSL@v
z<m5Z3uM=&#QJJcA2uha+r4l~rziVEpunM<xAk2DBM1yO3;sqqu+z3cwJPiITr2ZFj
zr0DdTgMcN!RVTDlzekJk*RK2GIRHCb9E3~a&!yQa#2iw&K7vV{eCYX#w?^{tE_C+v
zp@Owk|FC0y&VEeV8kobRba2tMFV_Od1<|n0zAr3bO4a2OXsXO|&kc$cpH5v`IuVGz
z?122ffqZCM_X{-SI_{_yJH4i-*)DOx3xiRd85yZ7&CEmD-T4ec%#F+0Oe}nNp&9Oo
zlIJ!67#_FyNOX4L+t8_2Ow`fBb?tPBw~r6$bo0t7qYbGIj~r^v6-$F;dH-H2QfKp~
zTF|kqp_62xR6tG$&JSZ6zck`bdGOMy-7l(uBTP&yrB(g_+$OI|ZLryGM44>twJnO@
z=3<2fDUC_`Dn))1D%g7$8Cd6VLM9i~NG~fyOX+z0?obkT6~6yKB?#_fG4Gr)DmL}!
z{0lS;t{Cv|59E{3hf^~8?c+TOi<5XV9}#KHj@7J4)0T*1aLQorT{p(>xRX?(qwY}W
z2H!en)Xkm7`F>ww-<y*y;-adcU{>t$j13X;$72@XF@VmEm2+cZwBBTvjif@ixjn=F
zCTK+=Ov-&;a4q>x+vzORoYolU6S6|Ml>0Aa?F=RoI<+-*W_E-lGZVkY+$-JYvF1|P
z6AYR3kAMrta`We9LrS&?Z<xfVmlPtT+!wSNc43Ho-yHG<;@2dKw0Q-jxhkpgG%RaR
zx(LbbmPuwljNCG9?kA=GaG%4&jC}oXzy<rcyX8n%w|)>F{kx>6%Gh@N)AgnG<8G;<
zFz%`zOr$@X)<M4|DpNje^Hj2IONPV}9VK0?>1a3sn^^>{mHSc{oY!IX1&J1nQ(vBk
z#y^!_G7kzNvw?G9^=`*RURAf?mm9#)*vxmAZiSGU@3Ht9=1B?sVrF;=0hS+u?9y~z
z-&;MqH)Q5^b;<>oO`f(?Igj~^Ryh#Bzv;Z8{s+M7rL!MB%rnlAJ<pISsptKi?S7;X
zDVBxbYrR)b-jMlV2MGOq-?!&uy9h&AeHq_=9p+cgmzV1L_Osek8@YtAe33{9P|_*>
z^3R?ZXkeo#s0SM=m)!X_GYhr(t4acAP9~pe<*qfM+x_-SF3D4GHb21c3vN0O7~j9Y
zvx3ZDIvW{yejHC0N;<b0l$fhw2aLMX{1Km7)Pw#=5*_~ped$sjtLr^;Z=c$$Y9;Y;
zG&MKNlr()ZqW#+T1*+}Nba3G&)g!c|;Wy8h+ds!{)z$|29>K<1hA<^^H5Vh9<aciD
z{@E)I-uLa+ebPpcySqFq!0yGod4u#e|NeC&<>9jNjCA{fi?jxigl;a;N*wNMY24kd
z&K|rcw@8xabYZtcz;b8t%3zMrT|ReVzz#=1^BnwT(RZNBYcvDPe&cH>wsCoFb$Z(D
zN;;T{L60`|t^C>t!o0`+%}sYHD$5YU$0MKhQcyMK5Vbr8fMO4xL{Yw**bz2F8O9E#
zm^hTFx)>>eS&)o$D~sb392TSz1LvMoKxetV)gihbbJw2P04fC%0=$aK?ZKzeRCREC
z+7~+D$-GP3zjDN{IPnX_&Rp1F&Dv4}a?x~=(T2aBgUZHw^AZ66Th|cyKJ3)pipZ<L
z9U1%?wmGba*@eg^OTCHuPc%5P#}oisL^()IcXf)y1k;~s@wJ7E0SI=bH-m=Bw*+xu
zAU|9b0Lwg$q1?hO(%A6#F;eq8jza-Ct7Rz?^tA4)F>1${7Ku`U#rJvbh(583fEED;
zP!?Tn&?V#nB3=drwl)r`Yx%QK;J0!0Zw{3WaeXdUQJDCJkp3^4gbWsTAXmXDR1{mR
z$wK_vUll<C+AbYYNYH%~-~{j4<-*xnINky+f*U}u%FIInZ<mq$6tvI#aVyj_hGs!a
zf!S{*bh%dMlZ*WR<zTQ}3t^-~`j{(E0zJT@>^#&_-9lDRZ9`0ri`9>H(DG9f!aKSI
zu<va-rR;bK-P4qiHUU%XdP8dK1&_=&RA%4*=#XN!&ZPuZs5yi_H*idt9$WJ;wFAvz
z%1RRqs>x}-^C}D8cm-ZGKR$<bXy-&ch`6SH!kb&a<Bo!TytoMh<mf%$Co|h_*J&-1
z@`2X-rY!`OvEA`Yf8oj^osCF37=qhE8j-uQ*oGwGH&R}5+V(#5xp=!T%`hdyemK!m
zK-=KZ0SKO<98Q#cEk$iio7j~L(@t>-QqKPXE^_Zx!Q3xf;X5I=c!qLHqRSwZdBFtH
zo!OCkO-nl@NrqJ@tAH-I!tRt!yLo6JOf4^hLLArBkojePwgk$&BVzeELX5ber%r1N
z?gUN?GV|tVtN!OHwEumsgcC1VjbDUc!x%w6%lzq+=eu!CXVVK0H{-f>&C>`XG~V4x
z+ib;2r8S3RJh8m*{6On$V_jUSovrIp7z3dInnAI!=tO%uoN{fGr<W~GuHhRdTu#mi
z2xxA|5p^pErDaE`GtO(2s`VgX`Q0?W9h4SodPICHbT`coO&R#gadK(#VN-yEl%NwM
z8&>F!tghiH4Y;#t{Is{I_QGq)|C^NHYaCEiT$#fW?-xOaze@O^zA>M@_)zxfku@q&
zA5yZ=AJw~N;eEc>$+4*4ktgZ=Pg=U=@u6&vSLP-B0}BHVVuZsfoB<v0g%lBAHS)(-
z8UEB#f)Z2Fqh&WByZF!xk2luLsuxo5dR(kGDqQyuO2f`ZNn`s4i}?9-vH6t=@54mS
z3Hg&dB9dLU&xz3;cXI?2Y4xnEV%jt1|G1b|)eI2vJ(JFAm>e>X7KYpqDL!PBdXIf{
z&i|q}@?H0NtZ9y8-4%GZL9%Z2z}@@~Y6WGskJF)9v;Q(&u!6h$Iw&hTzjw2~q{?}!
zN75s_r!iDE^SzKfMM}iFF`1b7?%LPK5SGH9|34g%7D8jCbk7R4@Tv(w%&aQ}gX)5q
z+2P^mRU6p+Uz%LgfC#R(sG+4NyekirVxn$FY=wi0QmVN#@E07==plCs7I&34XNTwf
ztOVQ~NU`b5mt75*MxFEhwFu1;dH|s@K7<8}dXO)&0|S2Qrnuy+D!c-r@lBpSp{tt4
z#Lv;3Y}P)kc(c_^A48iJybmSRfJ-nt{p)C2@x_bAIoaDpwl)Y&`99BkM9Q$@r+>%z
zd!V{goMlj@|IIA0gj$s3$eziZdv-5I%O^qB6jcMB$!$^Q)0@K?J(ra{`fS0qy+nM>
z_qm}?0juTAv4Qky!=%2|PV)IFyCKUUzMgW_Z;et)L)jO2dO$Xl$zq}I5hsq|qlAK&
z??jk}izjP90qNi6A*OyQH*MIET3%39HG)!G&>AQ=tlyK18Wyu;ZV@@!ru?|X<Jz*`
z^{l>tgyFMcbaZi1QlQzfn&l!HGs>-HVL`od2>z4(k!|w7T<S%&FdmVnAq*dD5dYCo
zw%tS7rDNZ5O|Z`#Q)SP}M2>hYtfHfWvqB!anpeX!TtneW1gqj7P})G~!d*KlK}JE5
zFR%Zv!`o^F-HaeR6-HasgD&S+MRg_;_&A3en|pZMQ^(UvD0@~-mV)F+R8~U4VBZ(-
zE&8$7AJEvKdQlfgBHHh=%K1_aPNcEEcnh}uSDg|jYO0oOrIgiH>Vtb}HC(KQdE&t;
z^p-s<ru!md5)RBZ))I*V@%yn&e~+iK2+oUi)+iXwberW2biCGn?om8!k;V!IqaA6>
zz0jZ%(DZ@+{ik7-=u&uIqqW<3gp)eoS%P+}@?feuT}L{h3RdMaGH)JMSV(7yf^C~a
zVTuOMQ|9~PG-t;Y(NTPr^r0|zLeXt{j3#p}wD}DWf$Nv6Rn;uDvx|A%!c&%?ujL!1
zmU2TFsgbGoSRPuA*8mT2j~mV|93ZQqTsDl-YZJj}k(aR|Wx{O}!7R!q3Ms42mw)rD
z5{RiP4CS=F&KXIrp(w$cY9r}j7EVI)NP~;vi2S&I2r;qyI*YO5@EoFA(-JK?_S@{!
z0_NiHLqdJ^1()b9=lqN1B{^f+-3=;InwG<^V-ug9x`slIX(*UTw}@;cWZ1i8LaKRX
zoli%bShpPlp8Su?W-A?ARgQLs+61<m4|I2AiFgRhu)>Sbf>E9Ag{idB2e&Mh82-RC
zl4h?aHgG!U5L4UNR5kX?pJeXa;jbyG8P!4o$JEI^rI%Z9H*2YV=V<wo5&`XD!)LA_
z8h=EJwQ>hIJzI&Xp|6_+wTD4Pw4Zn<4nC{IZN+uWG)Pei&8H)Dm>{v}6qi=iETg5W
z@HeR<)tE!K-1jg5zCM5G(3m+R2`?D7xuh3NL_^h-Ddm172bt`w9RzHM&Dw|Ib>kdR
zuPzOxBf1=b2&dFZ^Ws?3;O!py3MqC=v~=)C4nnZh>d>|_V-vI7m-YkV*AYhjtjT7N
zu5kY=+{REPIskj-e;N|y=e#4$EC$F=7&6*e+askRt}r)3^k{54etJ?_<aZ2T@o~Qm
z>H^yn=nVZVmML)4bHp{vyZ3?GuBIi2ol)*BL1MTfx1&?KqUbG{Tgg*TyuSzk+h(>M
zSM2`zl}H8!RvJ49H}2fm&YB=Hf~A8+NNLeg@mC$7d6L+BeU2@@x}=T2F_E|4I9+q2
zf`7+I*r-al(jsvH)m3Se3dKT6x0iA&h|9<{k^BUTq%LT}zAHxT`&rFpX^zlj!Wnhh
Yt(5IG8z(dHPd7kA?Gds<)iUJ&0d{MeJOBUy

literal 0
HcmV?d00001

diff --git a/Resources/favicon-32px.png b/Resources/favicon-32px.png
new file mode 100644
index 0000000000000000000000000000000000000000..7d4e80d5eb84e82a8aabe044b55eb6c5b9292e98
GIT binary patch
literal 1014
zcmV<S0}1?zP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW
zd<bNS00009a7bBm000CC000CC0X?ab4gdfE8FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H119M44K~z|Uy_P*}TU8i_pYQrFu^~;=w6y`LiRwU&#D-M1*p5V@
zKSBykX{b~rI#3i;egcAQ-WV7d!hjH605b#x1A|3{k~=VT2n?VS38x}Os7ZxdHBOVp
zaeO`o$JD;PcI?*9yPfxZ=X;;~bI!d&6B-&C+HH&(Q{^}?0QiBcAU|3blhf1FR{#=;
z#35BV2O0+=sw|7}2k5uJ1>3gQ%RQyh@I)f<qAC}FT{TKJK~o_8z}Y&j7C=XA+n#M0
zU~n)wDx&FTfa^(>H4z=OZF_dxhRI}dzcD%obhaYALJOcVYr86AbfQMM0-Fs46>9i>
zBJ%1s04WO?c9}>A0{&jmhe0N4EY6GY5@-;#ANbi>psFY8K7x2W9tK&gUc}8{u;Y<T
zCX>HgmzV+Km31Oa&dki5FOQSS<N;%J-VyPy`yEK7tk~hVB6m6;^@3y_jkA^ETTs6_
zTNoq1SB|%~=&GZv*IPM?9U0$mL?@Wd4I7`e=N5aj#-}HP8;fZXp4^Hv&M*-*&J7}B
z)aBBgYvuntaPau}sIhbo;Wc2-*8D&&uy1P3A2|&&=hE<jWdR&KIzFtbU*Y<;mmpSs
z;g{P1VLf&9gB}(AybR;6yAZ0ah45~$@dnW2GItaBs{(?RK;->G$p3mv0T5nr@zcd(
z;p}{7GVe0iQdtO5ho|0xp5KY^5`f}!c*W%|?(9T(2>`A-Ja2H_db61(BI35^8<H3F
zB1o*Nq3VZE9DDn)iuH@2bw4<ws<-pkzX%nUo&$~5t91n^7QK@mPjRdSQ`I^ys`2d_
zD8h0ed;%B=6_&nk7Q&j(d~^jN)yyKN1ft7<@Hh8!Sd|VRL;r&Slp%8d@b{}7kyo05
z@D3b#_Yh+0{lhgw(U<e=n<{oP+MQnvHUuDI&bjz`6+W}zGN$=A{I<3Cp<k!dKQ#!z
z;61MBnLmH{c&gdp`;i4is>3NnR(5^26X9iml`2Q^Ep^{|YzG1KveVQ}>&xi#?;U7E
z-rr6`9zXWZn7}7B7UY0pOQMB;y4IJ@I1el7i>EpQq1+roUrU1OO0!-0>nAHAtOYx@
zj_L@o)_#C{fA2g1H?o(m^+un$2=XjKcbg%0=e?mJOXZZ`tPXn0_KobNYdz7YKNXL)
kC=db(0-<(7c#+fp0rHYOSk463XaE2J07*qoM6N<$f*tA0G5`Po

literal 0
HcmV?d00001

diff --git a/Resources/favicon-48.ico b/Resources/favicon-48.ico
new file mode 100644
index 0000000000000000000000000000000000000000..295a1385b456709c7b67b0786c23bd129f90064d
GIT binary patch
literal 2926
zcmai!XE@sn8^-@JL(!NgRU=Y0TY^%1#@+-)TWYlSs9Dj9U9)D*QmdTUL{wX&s16#V
zsa;!Da3pqV>UGZf@_u-)>;3ZF*E2ue&kq1V04;z(0A~sV9@7JW0ssII$iF!q4FGhV
z)nKrH^Ez4p=w$?eBnvZrR%Tx2vr$$<1EkfNo?QSCV1~13NAoig0O-vPky<E^=+A|Y
zJ}4WmgzmOAT%UU~mv0$;2Xhs&$k`CbaR+n5(<ABTO1W;VR)wMAb8YR&C8o6KodMdU
zeh&*cBx@-|EF{^*^aG?^8177qtO28@JteOW*{kE?UDk@%O%mChWX}hUPkWz69TWr|
zxW@_0+#?i6x(-<_=w=vV-BL#Cj4B=rSns1IzVg?DaAB{@%gf8w*LCtPR$RuLmVuQp
zma8p~i*2eL`qjBu7#j6A?6P2ySu*1aBe~R?Fy`PO1hg)?{_fnHSIjnZw0iG7Ca^}i
z6GaDiU=WWDerN`$d%hduCP+4jnyGO~@yNw;J+|O;da@%qnG*YUA^x@9l4QV>j-WMZ
z_E9QEbd;CrKKI^~04LLh8%@qyTEZ@ZjFBHv<4&lb?>0=)QV=k%0LhGmA4pjV3uwm0
z5C|$oLecEx6KWMKF<V;F&p}Jnp3<JZ17%`gz<@<<_5(9=HElIgN_iF$0miv5&UoX7
z^WowRf3G~n&O+|Q=@=!-Slx7VMAxlp2L=X)-dlU-uXS%g?9@_mcE?PN>(w{vt?z_q
z?YeQk1k=5Mo86$nmixE*X<Itm!!epLa=T1jP;Kp}-1KzE4RjLY$g5Y|8bE!RpXZDB
z?mhS|8NWbwHIA(aU*9dmg@g*0KFaa&aa2Vd3h7mhCeG$}NB8!&j}^A+r}H$wk7!#)
zp-<L_k>T%z)JPj$MczFXqcr%>^3{6ys}o3~xa#a^4^g*>=R)%YPCF@4<r2W##AYIJ
z;PwE0;(61AMdBgL%GEiPX8&CLq-6=v^5(581yS&FxCb|uX56%84r$LL_PH-LM6bJF
zOtD!#SDtF*<n|y|)bzfab_eNQPGJ+U^GU(fYV%QY_h5^j(+jh7x5{AQS9G;h^+1!P
z?a_O}4E)}ta@L<c$spp5@gIZ4V%zbW+Xl{FvWP2ZwjzdDO+>fwD@qs`?{O_=$xqK%
z>;~?}Le>-FTShJmH2aFHAMU5SN-2rI1339ABp3znQbyciGcpo!4CY2IP}n9HYBVsm
z=CTk=z?8VyC#<N^_+gk#7DsCDc`3@3!R&)ukERl;8K*PBq6`j~c%QyEX!G@sw~pyG
zvElqAS?@#RCo*Z?iC3tPHTHa7%TdS9z0pqM*WWRc)X}_?I&1l@6q*0aPVf7N#mdGH
z;8M{75f%*A547pVotA*T!Yud88rqFVUzZk#Vp^TH>MS##eUu=@)m-V!%Ix@KppkoV
z*NwR8!0G?Z3%UQN8&|6C6Q-|B{=$98iC|@-6^P=86ES;|G>d3VzKP_|d($k#Np(yF
z9J5|Dl<wtwcv9+Vf69(3+eYXex##d5&Uw&1LhY3zRqmHx4I}*;S;Y3UI(r+Y@{g(c
z>fn}!sVWs)GnIVC$a@`|l#&B^d>0k%ap+qehjP{A3!6dVl=mPhi4E^-c3CUOl)|kz
z`Y;O0^tunCh56{z4OsH!(k<^C?2Uc8S~(IYGrh=@=UJ8O&%7BbeQCd(@2&e5Sraj>
zBr=OLUs)2cvDG(jhA72(aW~w%?NF&bN}IJlcJ<n^Uha3C)f{!;FW26nWuJ%WaU5KF
zK`=O)>Z&)X;-;sUco>rTUA*UEkivP*zxt;{;ZL8kpY*F<24~F|s$ZL*n3{-VTp$T*
zd?OT6hOT=JdKoTKJX!7{<pK|LsK*7R{&&qDJSvsJ3%U%YS?QO~tao)YU3;c6CmXu7
z*@@Tpr)|aT<$MTUJZ;Zj-hJpW_4-NKD`bZ3h^jLm!iQWru=2c-GV6T`n*Oys&n)Fx
zb4FOeP}dCEpzR#<KRA3wZUFKRhXr>}@0?+K@NW()rln<fdRtpU2cm{M+cSlUQlyM=
zHnz)F?8UQQvXfeW28WQ(XM|?5xbV_BdMOi^R#sL-j+$$-3u|GIA9Hb4g1N<GD6~Zx
z5%NigAs!BebIv;^+Yjb<e-UWc_(rv+d=7Fh<V9PK=zrw-Tqk(VSC9EdDBn(%)p~qr
zkzB!$@Q+8LWGQN{;v(`y0-r4FF_-$4zqQ@@oF;w1=a=IQP(h8nYp&rz?z;0)4jG$E
z<Fh8#UE5AsFPl5Q=QAQ?GmCQUDV!|lN5q6ltM<+ntF^wMn-kL!95UPFg4vG<92x^i
zG!s=3*)>s93ct-u>=hZawCo3BMjH3kV&B$A9J3vD0@s*f%illsRc&+@Pcs0H^!&QT
zrlo{VCZ<}I>ps`i4=-=b)4=AGqKkD4zR|E1Y&LjA@qV4Ofa%BgZ#T9+1K94OUstBu
zy?I+JzpdFk<-?Fk7qf0lm12=!^*TA8#+C3f-`41&s?z{gK(9b#IBxHaap7atBzL(1
z6J-tAKp|9(*LCaq<^wtVQvo1wnCrXD+Jym(MahT5WGt@=6dWdEDLvewVSBq_%UCcf
zW*yul7=SjIJAA(-m@VJ%i`J07-dFthxtIUP1F*9`>>m#VMMUfY0Bz9U9=H+T`Bcrv
z(FU4Ox_vC&EM!W=wSoRjB59`eC!p|?>Buq-ZDv!?8kDfI1sCcOO^nYR6bkLdKyCqY
zbab?_y75^mh+r_wa}K6zxNePMyo0=1_CR2yI>UQnS7+$vIN|lP!^kys=}8cwQ=5~Q
zmv`DX^@8=PA_6#yu7znh@MM4a^2H9|Na`1|QDWp}gk!IykQslp_t4MaCkEB3M#)Gx
zrCj0<%q@8%1ubhBwP#AXo<~#PXQsp#>KuSjk|<xli`g*Dm1pP2`JzK-5<a@!@0L+!
z493IXXrFs7<NaE~HMm^wB~1}49k<ufL--C!0T4e24J|NWr1^b&BvDJm>k1)-%;5pS
z2_8+^KgWua)MLU`1sLq>Zuft7Zd%SX5y|d$%*zKY<%Ymb2tz}~(oeEKu7F&F=h!KY
zC;SFgb(4$j@Gxb;{TMg?wAS23M~iMHN9d()yYzPa4{Sx<grib2|Al#fq-g$U){CWW
zYdq-Ifojp%iz%M$C9}zL1O&T0ffepTTpcU+UOiO_ARisoGzYUS$~zB_|I(Cd^7r~I
zsKhF}Hb#zi2l1&YtJh(j7LW0HFx!WVc{89pY9V{E0^HO$EXxhAA=vtOvR@O0VPsi0
zjPt0LCMH*Ng{uAGG{P7Jxg9|H^3G*(GxDzNw(NwJJ=ij_dM>79K8ehMSqch&w<`75
zCNi7}Xi!%55BL_*S|9>n!mnM-`eP4nP=_reC7Sc0PnweVQ=W%tD3)W}cPF~R+)qnr
z)4tY*(2MbOW($}H*3nsiRhGh1ws^&Q@53T#piJCPyZm^13x^|v>4m-y!K>-Y2cyZ?
z4})&xvI**(#sWD)_kWWQYxy(n3Zuzvd=CyGE+lH7yv}Ie?q-ohsO<DJCl&=dm<yrf
z=mEES>w@fypVxok+YZZ|s=sK+gL@X#8h#XLL>+}=rxs2!>5<qzaineN!*<R^cTBE?
zKILl2e1s1pXs_i9(R5?*$L==2htAx*nT~+Q0U^IfRGw!A<6>MMAN6WHkDz#7ay_M5
znWk)(Pus~jhGf)w>*t23q);8bN?SS!+%*0OgO1u(1+KT`HSFt^c7S)TXonREg|2m$
y2^p{E%-69+d^$+mtnyYHt=8av(k2F>VWKGu)eg`6athkXQoTW-oACJWU;PgeO<Oqt

literal 0
HcmV?d00001

diff --git a/Resources/favicon-48px.png b/Resources/favicon-48px.png
new file mode 100644
index 0000000000000000000000000000000000000000..238c48b9945a9934cb4f7e5c825d2ca860b9bd5e
GIT binary patch
literal 1490
zcmV;@1ugoCP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00004b3#c}2nYxW
zd<bNS00009a7bBm000II000II0aj1YTL1t68FWQhbW?9;ba!ELWdL_~cP?peYja~^
zaAhuUa%Y?FJQ@H11y4ytK~!jg&6-<mR8<s)zqRMI!)1yTZJ`CZ3Z+DwU`ezRVSrd^
zXo+0pqD6y2qe)4iLPC7v35|jB#si7OXiy>$lHzqJb&6t5$Pl6=B47hVs0dWrX)7(v
zSw6IdV&|OcOot9%G7ods+H3!R_L+0`WrQR#XU?2_W6V<EMUY9VWQ)+ON~4G#Q}tBW
z)g7$?w0FCrqN1Vc=|h(rqb0UYp$IvEQRT9ToK>Y-#6A=Z2Cql!SCSxnk}=jc(3SnD
zcM5cKFc_?Xt!Cc5x(Zd^14hSa*Q!dD*PHfXAP{Kl9T<=Szu&(QWFPQIf7_^Hi)fXq
z6o^n9Uyt%D*o%X~;Dx(a;wx~ExpU_)5z+mCC(#-SauxL19uo-}Sg>G0K_p@y2Yks?
z)?ZZl#p_LbE)WQ`nM4XiBK9s)BpyJ7g3iu6n<0S_@caFxAcvDLD{fjrCt2~glkyk|
z5#WFbCsp-vkd;7IKZ~Ch;V^JZm11DFORf)iDXtOl`~8-x+z?@eLrRA+Qe0bGd#vX|
zadB~WXXl-7L1)G&|4}&H@=hoey50R<=FXkBMTBk6P4sYFzX8j#a$Up&vc0!>fIuMd
zw=t}9tE<YDj*eUFdx;0oy1MT_0Ihd!X{N_zz_#r%ZgCMk6770zZS6_mhNBF_@%sAu
zf1`v{L~5LS)ZCcAfx^PK(wehJjux@UdbQ(Jd;96nM0mv^J?iVen`^+M(E_&3HK#_l
zE!RZUDLmZKz|=*X78$YEv_gr8J0k!ZuntTf_GEc``)OQE3@UvL<SpJ*K*ZjK*ioJU
zvb@1#@i3XV&%m_OjZ3iYPY}14)Wc=su7T-g8;e!t3!;olUu1cMCrxr>Lb>Y2l$x*r
z^2#<%QtW<;zss^lU#3ouWTbQ?x&*8JK5<Nm@6l%@1|4haq7C`LiUHJE*67RBoL1JT
zUPxeSGB7M-%u>{KQrCg99XKjTOY{`sk~P}fQ1fx~gAnP4g|g5k9<gmHKKJ#GV=g%<
z#Q+l=qbdg<X7SKvAlor2ga%JaVi+Lo6jaf1gQz7T3~<>gD9WlQO4j#xF_IFD!VfO-
zEX$MjU4Geyrw7`R2qy+wBN~>+NVRQig;RpUOjQ%gE3Iq-ZYJ1Iy|y;3gG*_ks}igc
z73;U^9Y^v?D?>nGjNDP(pG27aS4QT@;iRv~2%jgJCJcb8ZUO9M6^(|fEDAIHQy9Ot
zdp9pB7=W{1?W|FeoyjWdgs3dNl99O=#;+CO^JLWQS`%;GYDD#^WE6}=j|tTYZzRJ+
z*K|76QUUR9v?flEiQF)+id;k;m%Ont6a8C<09%r#uAlUnXle1S1Nk!1I#zl{_+^wF
z_ugm(epK|IL_2b#EqCht(BZ1Y4Pa|Yj2+TFZ9}x;&;2{2?c1arOGIb6?D3xVHMYL}
zduaD<r-W2vKpc15RJ1zu5Q~Sd$EfoSq|!NfQWC>}HaZ1Gd4VJx1U^U%m@|%1VfOUW
z$}ObOgfZIT7ME$Im2V2{b}u9Kq8!0x?eIV_+BqiNv`^&X9#vLHOR*3+glZM8XI@&Q
zLeT?~>C!DU`bL%Jv{K&v#_nUnbK`7%d)BlfFW?_^f&F7f*gIrKMu+!xk>)ADeRDPs
zK<>oZCnA=X5&OhEB?yw=nm#n&*V)&E2jJ=-Ki$rn@ys`B%W6sC@o5ZZL^E;sl>Pdd
zpKoSPd48WUwjW~5jZ#OS+v&c)2{fHQ-ZFOL?7g-}Zv(S{^pwbS;s3Xj<MVx5*}IRS
sx<rsN&^%xqFl_Lo#^j){@!V_w0owfI-wG~ir~m)}07*qoM6N<$g2~3kqW}N^

literal 0
HcmV?d00001

diff --git a/Resources/favicon.xcf b/Resources/favicon.xcf
new file mode 100644
index 0000000000000000000000000000000000000000..ec49b922f6b5f4431f5371e5e4295c7fb4ea0b75
GIT binary patch
literal 32098
zcmeI536vd2dGG7azG*btN76_dX=ZNU(Jqa4S(@&VEL)m;je&^8j){$J3ERlh^vJS_
z6Eq<N%p;Ewn>=E+<TwUQ5)!s|2#-TdVzwl#c7m}Ta9-Gq0dHC?&D`#OzkgNV*0ns2
zi33SEPjgOBef8DSRrP(fegCSy`>#20sQacX_jT{P=CxNFV{++(W6U&p#F@d<H1u|3
z^zb*6r;ewIXLd>}-Ynw33U|tWvR!fI!2<`bzWy+Nn{EGrs}Ems)fI=YSaJ1Dhr=tb
zJbW<h-gm{3Ypy(a{mSmE!mDrWzW(49*IhljQVBY24%ZyGV*k}E_J`M8m1dGNh0(*W
zd+pWTqX%Dm%~ky^g?;<4Jos8NDp+Slxc}8xbgy2y+Wt*KtVx6VN&4zZl2*dIpPNw9
zn??^^apg7F@9$nyc^AyrO~y=lD{i>{n#0}KTz}<jt{zqTgZuW4UVS(#pi)1Z<v7nw
zs&+{Ka^EoK)zF9DX3XJRD%>g&N+L~b*KfFqN5M5|oCaJ;FX52{MJsM|O5>QWD!QeL
zZmpu*s^}?I^i*i&Tls5*&^z>3tvQZkYTn`q6+w^U*1X@CD<0uF{>-qNCkg&v#<V_x
zc7D#7x#t<v{b^(R7aOzo8e=wn$QU1O^<QMn@KR%5e62CB_$1H6#=KJHFh^VOHnDHE
zrT+p^gPC{GoM)n4zWxXH|B(GZsQ)>Cn|-1kHhy4^nva`P%X{;A&+9X5Ow^zEn!UI{
zJO8|2a?y*1OT(o-X0@3Z8!iqPn~SxRzQfig{iliqDt55VL_?y||GuT&{~c*$Ta|;O
zBctKSA>I;=9S&Q<_>is2Q4{CXK+E;-<@)z}{d>RueOcM&p49)@iT*FM|F_%!CHwzZ
z?Eg#c|CLWF(SU2YIZL-)y!U0RdEKd+<uCuu_ka1Rq%Mh`+{{Z?nDXXe(68}h{mTvJ
z{Ae&R1rzV^XZU?cArAamevg6cclzBX-tKq#3r%^*AB5<l&fWezQ{F)y-TqwD=g&c&
zgMP0++eAd^^Jkj)0>6N!@PdASx>@c|Gvv9<?+|CX-)`c){z`v}S!16zv(|4_gf)JP
z$qlaco6UN^$!y>u`Z~YSZ1fvUyxHF%C%~y!SvSfV#Ho|B31^F6Yqr{_MyxG<>d?Tx
zd*E*MBQxX=sFKvwHiPk3`%$pVG?;P9Zw77IR{HTSLrDXFDRE7QSw?II?S9)@)6G(<
z+3(LX1;v|bJVlx%XRmeUn7kjK&-k79JD>6N`SFmML#q)#b9hUS7@Y=ciKw}v7K@rE
zs$0~YbE5lY7Tu}M!O&0;3<dQ;JQU=DeiQBRgXw{~+?-$u<HC+$ZqRL_?L)!*V4=wk
zEeyKMk^lwR5iAMN2tViv<`^%SEixa>G|>fq;01KF9YKFEUEHO?G_xY;Fe`(06YU)e
zRs>VTUWKzJXcM_QXf*>ti=hl_gJy9C0(zPsti#z5G>TjwsKW&ta5e?%ZNWyI%>i9)
zDA-hSY87>JkUDg>9l@5M#+(=A6tMM-vn`;{1#5yb)u-1*v_US24IKJTgPHIdK`CjD
z;edq8#cih+j2c7KE8ao3TShG*eO5~anvWibqmEagHN3#~y1WADae?i4ee%)6lxPx1
zVflc*X&;6$+POEFZ+h^rFh6Zxwf8sfy!+noKXx)X_0w<P`^AsG%@oXde*aw$$H~;B
z&rG!a>K)%rW+qF_M9ukk98cyX-KM<YmhUC=5{Am2cb`tWk}gwTcE?1rFk!Tu@vbQ8
zPC89Fcjb?hB?&|3s?R6A$t)NLPbYl|vYh_0#7kzFX#IDRLNeV%`^S_1gf@76vMia(
zMDRb8<q1M<_+YXknPQ^4k0dLTHu&#JR^i5PPF5!jqt_;Dk`@z{BHU&Zb^SP5n=}#o
zbIAa1{FY>0(rBU!W84N4HGd~rj~l-!*^tzmD1XYj@n@2a39{asY)Wc*!@bsxFHSb&
zjlY0{u+LAn$oULT&P3g@b>pMSR=n{8IFZ>xkxq_(^{%)5_M30}=z~cP)*n*%iDQ3!
zL90gF&ddJFe4pBtAHTW7P=x|?bmwdhyVp|p=;W&ycjG^!Iikn6p#<@dX`$%ERx{ne
ziZ7NjB=Ord{3g-Awpus7gz-0@L=V+kH$IF;^bvWUb>n-b3rYM==Nxp-#m=$nlEho_
z#g8RDj*12zLtZdDjmX6!=UTE`<UC6*a&dD(#FA#clo<UUMAgR!eV?_?3Nx|8XKrcn
zCkFk&?Iv#~_6E$Ev;4)T>@&~K)8x`+=TC<E-IPF6s;1KN(4dIi;51X-29McjNNu&(
zUqwgS%eqpNrDAJNV|f>yKgbjr4em0?LGxW~{A~pJM;qSfmj}0Nt}Ih5f|bfnqq~`A
zNsNAI7N*0s7@X~T&!E3TLG;YsOx;jZ6=^9i@ZF>sAQ)C{O!%4*i9kp7;}TB1&G-D-
z%#id@1=#6P4jC~2ZO^EkFMwjHaIUrgYg)Te1G$vCyn~UuJwTFq6f&4?1;PXdcRuu(
znXj(U4JnPzGr+Sn9183JPRHJ+p<c=y?bQs$7;dMdwHmP*lg5~Tn0wA=!eOLGnKiwX
zed7m58@|I!@#R?JZQ?)Cb~9-;GqG9a<*no={avkjaQ6^z>R?78;|{Z2Go>cXnPw>u
zahX4yzCzjA#Fs)2F~y?2jw59ynl$x!ofb23?FBt`3<TqCt6p;Cb_Txj6Q6zK9!kIz
za{0UeDNdS`iRf#;*{^}1VbA-1luS>`|2S4K-DaY$bjRb&CKK_eOEt7#dFJcBn=DAm
z|9q&OCM>Ud@6!xH<)7TxME@!;x;<j*D1ZBvIeKMz)~%;CX#T@ms$H%feTwO#JpP6T
z8Z|0>k&!sMZyn8AuGx2jDWN=ZBxe}g=Y5_LKl)IsMuW{iV4NTSdbhdi6oYy6;}0Z9
z8-6W`%Z%mYkKf++-ZHa7`A4H|iaYQXCXjOc&JK+@jqf1G=<ydZfJ9-;bQJwyB|}Jj
z9Wzw)lYR{&e?V!X@vV#_QR7z`=Hnw2IT|Rd#NVjXF#pGpc7+|~BZm3-9*rQM7IlfJ
zUrQL&kF|)L3mJb>BKl{lAKxtM<Dy<7>K&p6M7>E=pQzW1TKEE}7ITVWn-N$Wg>eSz
z!Sl^ZGgagIpufxXn^_FPgNU({0dc$OF>~#PYOz_UQG6i|L+x%x_9cFo>Cu>{4HN3D
zrN4$yH#`<Bt${Nd9Ab83uQ9~1%6K~@g|(qm%}PxY?Kp}w#jI9nn+EE@w6cfdg)MRh
zan|{IcwG~Y=7L6}{nQ4r8q7w!ry^Os*`&hM$r;4i?8lqYiY>m#XmGEofhMFT-B*!1
z2T97DsrM%W&1q}sgOOh%Ti&g5#Y1eM5U6&1q($p}CQ4@Sz_;^*sz=w;Sg?ax1Mvjb
z(Qc~W%@u<ZRp95-*`weyBWo69@muu}L#*d^&dKhVS9UC*hJq%>`yJ|W(}T!%x7ixs
z^Jb1_Aa%F-4E~GF0z%c>7TKAm%PiJFKcB5r<g3H=+QgcGU}xidx|>+4DF~I6Lywb_
z&J=Vw`PvOriS2K4FjlaQT4P_-!gQm(6uCB=b((LQ%zDjLjdF(6=frG~qZw^epvU&N
z&CG0@1KZhfhScXY|Iyv#qq_x|GL$&ScB+P@&IB{=5K~;GuW61=%f*`0O{6L?YcWEo
z)K;p&6smobn{s_>2}9E!vtJ>gv#D5iBPEA<&^8bz4W>LjmXkW#cGA`SpwI3=A%`+j
zA!nY)Klyj}e*32<pM31m`#<;5x9w+J#ekWNKl#PCT-eHhIU|`Ezvm6>CB&rs&DSl^
zn8{@LnM>=K{koF!clI}G++?B|YiHc-PRc*LG{-V~NfP~aKf>%`QhRf&(JDTAU^U`o
zT6lUt;{`i{4|gy<FrhrqLtU39(IaaSDO-wTyJ!sNpVKdA(piy2<-Hn4@$X`|Vzzop
zQzjGSqaFk4nk0I#T_b4{e+XwFiT5Mk0p7Akqba$!O2A2ci*=%j6_Rig{VhYH*{In{
zPV~c84Xa5!Vx8!R&B%Cj62Hbe(f@6btdsaM=iF_bC|<1LHHm*6=R|Vqt9RXY%l~-K
zr@znK9p4Wt+S$UZ%Du1s67y=jfwD&%KE}Knzfp7P1>Z@QCh_N~Y1HwTnpVfBh_1O+
z^XeW6|Lts~;<rLYHJ{MD`VX{LG~;`kSdZs4vA$H?F6qK&HO1}}wNun}8jruFN%Z$c
z+3EBXqBe>5C8((GB-`2eAp{bCL)4E&eN5CbQFn?O7xiwEpg!+<9tZ14zTmZcOyC7}
zhTaSre|t^b<INFov5EV=dEzZ(d6W0Lyai^l$9l8hTkNqv>neClJa+e<jFJVf&!ezC
zp65}cf=6z0`aK$@;4Sr-oATZ=kDbjjZw1aOuidQnn91_qDv!lW!CURIwa$BMJXSR;
zytO#%JQk}3Z@^>Il=s%*Z1Aw1<h}JCYu9Do1~~;B)}~9njX0Y<w$yoV6V4WoeRQw4
z8E30~*hm+=Egr@bkA?0svd!gVuTWdaQJ69|a1?W;YKvXOi&x@M(WPFTS1(sh2%2Ur
zW4ghYm%O{EDD@?^7t`_J(@VU#8?nO4Beep}1Gy41uPo?I&u~1CQBzTEfE;q`gQ)-#
zE|U(Zb+g?1O>TH<5q&F|@^G<KoL=lR(Tj$Qv&BPb7Zv9h7nw!H1tuOYE-GSgDPq{#
zTkI)f``lCPE%FM#m@ks=i;7;6bwY85Syse|Ttu1omWs=Y==$E_;_@O3d+hZqa90<x
zZWR$!skpkxh+JfIIJ}nsykl(<IhBe7Mb@U2NA89qf+`i)7m?5f#SJ){ib$zc+=#Qe
zh?Itln{c)O1jM_Go8^>n5Ykw2i=5IK2XXBs*K%oi&v5N9zTx7kVtkS0Rg6k|>7*-)
z4EgO;fK3L{DixPf9ikPA@vz1zDo7Ru>RKSh1?q?;T7cN%VC6uDrDC6Av-wavA}wO>
zvR=B90+Ev<ASWfvoHWcM+&wlmnUo~CVO}$vF}XZ4IucF^<58p*g`?qgp%c9(7seyu
zEU_1xh!}IlUT9!02<dGhz3ql@aftjz!k!TMjfA}+@*4~DA+t+}O$*G!3#rgZ*dHRp
zP%<0|mxXk-v2aBw5sq4y7%M}h7&5_z^tF+2O^Ez%3fJPS3$b#92+lqTZ!}yVBD|4s
z1J0%p;f;hFaW;ntZzSA=vn51oN5aiGTSI2eu@PuQHxh2aIWI&~*Yb{<(cDPgXwIw-
zqmd&Bjqv4=k{jew$iwPvH$c)3pa5zE#*5LgK<h|+u6hM(+pjQNS&#aYJTC@9RHKZ9
zeM*1?Lu^uV5Sj8sRtlgaI!=X@m)G=G0^=K8;!zys*l#A5?7QQ8Nh_oGcH^0e+?IF#
zAnC9mPcOE}8z00xsUS>D=YJ+<Pbv6kfhp%oUrgpF@o{F?XzzWPC+C}}@2{{<&Nb!c
zu~Y0XF?C(|FBrgPn{wS)#K_I~zTh4v0wh}c36li##%m*H6IL6!F{T~FT6?=QQ#O3W
znJH^Ngr$#`dpmO#ZF$rgDfe3=WwhZmE>_BE4?8Pm?jvj<>8FRSkuq8}j*FEt_jzlr
zjIXjr%4qtd&RF>_XRKTnIb-Gft+6s%GU1Gt?{-GZuG7wFd5g7HMm67cR?A(^YWXM5
zYT0(2(w&O$d*{K6cdi?#9Y865m1374x?!Gl;SSj*|K*ZeDZ_V}M$7TrTcr(snqH4z
ztlqre8YeePB|b#ML?^dt<oy>-q_2XCnj_7hcS<c*K}Ao@75OqTZ&t_t4W_~9Sck|z
z(OmjRNH@M))P<t%qw%BhR*~P=4Ej~{Bz{8F2Sq(C>TRORqTV3tDN(;8>Ib536ZMd&
zkBa)5s0T&e0~N)c%K9(#zRN_tRn%WW(Rl)Yk<U)AOBCDL0NsIFAc{dC@Vi8zqkSxR
z@XmMM$SXKMCQSwBG+WyUa~-25V{|%3t)?6sV~%6gWie(uMt#PZ<rocFl9`Utn8ldk
z7)=>tx?=zyr{$RD7%f>I9gfkO#hB_CZ5gB8F{WgUDUQMNBrSTIV@%Crv^qvd#%OVj
zX&Iy0F{WpXCdaTV9ors_jxjTf(cl=fGDf{)%+45fj$zkGHji4z=*&tA?kl;>&64CC
zgT<5@NSk_v&v)K(V0~TA7Y*(pXo2!tWEP6I&@7U(z;w&$GTnvPGe;XP`}Zi&9w+@&
zfpVIbtwr3bkLEjDk&60cj8uK<T>OgeWWg2P$r!2bG`jc|#mRyzijy%aijy%?#c6dJ
zRJ0}wu4qlhsAx^bNVR6F%b=n%S#U*VGDbyZGDbyZGDfO0GhL1qeaV6=`jRm!`jRm!
z`jRnHeVOZWtSCzsTu~Os$dtu-E6U=0PFWTfyUju|&=fgniX1d$m{pR~lqCVn2@ucj
zfTbb$8?$1)SqKq@hoBZfu@(`t3$n(M^C4?B!Pc93kadnb7qZ@wosbPK!W_s($DIw?
z<j7f&&5oQ2Y0qTTn;DR;jyoN)&5_d}?Fy;hbU?Pde5XQAbrISjI~+L$a+)LCAX!bR
z=&g`5T(TC(nU32GX_r{_rU`Pk<2FLhabyEzr%P53$r?<V)j`g4WG&=;N7g`gIWo7S
z(=1Rj`l&dyz8qR#4y`{LA52?cu+~BWSgZ#i7l>S7$u5!Xui&y^u(;TEUM~MU=hu?L
zG3Gi(Ibp@&IGv6I>X)(RI982ainv&_9jmq~)-1>3yh@gEreoDtC7j_{4OOwGJ62;A
zYno#<Rk1o8tGOz#sgBiB6|3E`-0I2|b&6xPRmEy^tSMD_wK`UNRjd}rnp(wbb}W`_
zSsj}kYg!en(XpmiW#8afGpb_MJC<ARxuVuN)~u>nwT?BridExSZe{2a<{YatjWv#_
z6A6B9%9wy5|Gd;+9(YXN`ITHZuf*47%e%-duv}SlRLccsk#$kNfpn5!g>Rw27XkpX
zS~-{YTpMWB`hxYK1q0s;PyHWx^SQL=`n->YRT}ubfb!3PIiHKdK=5DEkai4t!2mPk
zz`3;N+Ca0|3)X`cmRxogJLmZ4n&4a${M&~@elZ$y2LG<c!vFuFpBK-i*`E*cJs0I?
z0mgq3tpqCEF9<?VG02_ZT;MD(?ggyCDd2!D7XUPtDGab<33xfdTr}|XfYl2m$$Kk7
zs|)>rCj;Vn^+4Ki9kT7fZt!9u;ajo?>u$jU<k3Lh!RB(f^;5yqLD&lxVUK+rV4^(u
zxDa?CngW<SP8SaNZ$Ci2Ek-qLuPAKumSwR0dJER4fPKLM_PuA`ETsO~Xnj6K>#2`#
z>%*(70Q<FGxsYF$uge1=>C^znC|ZH)a|Mu2oh<+=>c^qwELz`38!qD%f^BL5qh(c4
zeYX&zf@g7ez(b+zRx~Nf-Y;x?7;GG;WMN~A9&`GSWR9`aA<N+D7HZy!6AkZ1SU}Nx
zI4dG#dXMB)Dy}S+hrygVKUkb76^o_ex?#{!VcZviaU-z`_$@fxJ%m`ieAwdU@c@nx
zbOgpDjC~y#J0-M^kHY~}xWePtS#%zG?JZioeh+v(#SmnU^oBF^o|0SKUdTG~9=5PO
z?_DS4ehlOuXV?MwyUzyj3D}sH)B^ZI?m@Fs0N=OBy()NK5mD{sm9?e#BC4I^2mwd1
zgxiCTw{z&ATq^3I5WxLKg8J09QE3>-(Y_Z6`aOf(_Y0@D?Jv22+V7RjENX9&c8dky
zAl?XoIFdVz1L{kXy+V=?KrIDAK7%V;^cl^#Q6Ti_0SW*vJvxH)2oV`dfTQ7R8F^a)
zv;}G-wu9i?g1q$rhL2h>yy6_eM|T^wV0pX`NANk4vky`m4cCFOgUvh0oo+J<0*}B(
z!OC$2%Ofeh)`9hKRx99;!|QJ#HUirl4y2Gh<ylAO<hWkWhy(5qoB{3+yRxWA0`1cx
zBe2ou!tTjri{Se)UJ{Rv)Q?#7o~E(IT&`wCXta=o2XfyJd5HE)p?iS*!w3(sUhqAK
z01(6|&7Y$8{gTe8p!;i`xKh|YehtuQRPGbgo{4&|sKvr`qYWgFYLAQlN~2`@*TR3_
zA?mkv=4w<gx==`Q9HKta)JOpGA2dp<z5;w6eQllO`Wu3qqd(3|um#<diT=;!b5*PR
zK+7lo`Qz`n<;J=j5%E!rVMksJq)kJ8$~i^rM7eK)d((;strJao6jU6MAFxhT{}7lt
z_}a^@6XpIMM4e6x;Jz+F?!RT7sO~G^@AO*OYk}$GBi4y>9|Wy$1=IdA<>A!+qDKMs
zDeDIuw*PAm+uwZ3Vf&rmbJ%|E=N-0xqr>*SlMdUjxyNDqqYm4j^`yi0Z*$oG;?oY>
z|B%D>SH%w7f1kzlqnS@zH@?|o`cYkq?Ux+3e}}{N8y*MSKYjd@H@|d!?~=MDD9j!~
z`}bVhtR{UQZ5@4ekfx4uj|kcyKT<1ovC#fc?BU7FC9og#Yqong@{Atm7^;cymz3id
zLq=_}VElJV%of}Kk&Yj7hz@2b<v1dZdAv>J?ZVY>XP0-h=CkG~CZECl(OT|H;~N}4
z+Wgrf|5HDq;owNcH+UnV$>0+Ce_pmqUHj>k@rhFaDaE2<(C-ls^leaAP-U8`!0dzm
zV#fex&x7s|quVi>b;@nf=b(V%H)p||7qGUijKQ%1F~Hc<3^+X?1{izF0R9!D-Q~#X
z0q3g(1OJKvz@Ekj{uKj&J!Q;sjOi|e*%e<Uc$Q;;uBY)qfRzF0dddI+76WuWWq<&S
zF~{XNt>Ux6X&AV=BBx=1>hg8CnDa?D)sbD0z`@FG0VF`M$c2!gz#<nxg8z!_hHP=<
zV#sDkE`e-vWDjJcBYPnMd6ldWvfhz-NPt~&J;>S$$qKIC0O=~ApMabrmqJFqE`4c9
zvYb<q%eB~ZYr7SkLo`QgI|%*xSjvBP?Hi7Zlo$0%wXaE-bf$VuTIAT^RQp<7aH@J1
zr61IB71g%Qg{4Y2#raZ=o04UeDxJmW2mMszI$Y#b-KIHTs%z7-q^Y{iaKWjrSy+D1
zPZe#J3rn?Zw)3TG#PP_qGN~qY5}bw!ich)oAXDX<=TfBlHD7C%`ihEmIbW(}3!E=i
zG_ZKRDb+O~d8JF$4P;)vRO1#qU#fIVT)I^6dR$nldcDq<Y9A*Rl}oCC_B>*JMF$I1
zAXPvXNs5;0APXq@oEoyeDlxz=lf!l9q8>=jZrd{hRvBNIt(OsRN6@Q#T#R@-xK0hl
zh^PD2R!`FN0x-BkAjN6YIl~>?VY9ww=Ue1_EzY;l`C6TCf%COFUzhVualZM^*Y14t
zoNuc0&2_#G=j(L7Y0fvt`KCJ`cOX=<8O}G$`DQxbOy`^Bd^4PHw)0JQzB$e}&G|az
z^MelOo9ld2oo}A=aludTo9}#6Dn3?G^gcgma}3TMq>NU_;PgStXmN~1E`#QZuM*tk
z7#vJU<2O3S;*8PY7(FfnuFtDHJy~#_V{lS3&7jsXSh=N)8pmK!mom6rqCD(bOwK&{
zK_D*+afO$C>tjJEU#gPwpptU9CL)LJlpM5jgxhTC5cz_QxU4#}w9(cRI;OXf2|C(#
zOgE7iqOT&kiM&uZks-N>ypS?C+{i5wN(qMx$(F+v=^ZJHi^*bfBI1N@dnp!2J;dTf
zL>7x9A7XJLB4cs%Lo7~2WGs$=h{cJBjKwJsu{aTtWzRVfu{aTt#o{1{Se%H+SR4cq
zixUwUi-RCyaUvqii-RCyaUvp%#X%6UI1!PtI0zyZCn7Qy2SLQ*L`25oAc$C;h{&?%
zAc$C;h;XsaJI2+1%i=^t#^NA|VsRoOV{s5fEY3u@yhzAF5V1HGabidf4U2;yVsSJg
zV{s5fEY3${EDnN*#ZieYFAjo;#i@xb76(DZ;s8a);vk4v-Bm0Of{3-aDlZO#h{f%h
zEFlL$#OkSHaS%i-4uod0I0zyZD_F}Kr$UJstbR}U)NlfZ{49)Zgz|aE<Yme1vcL?@
zBE$OZT^zXfE)I$9T^#)MoF&}LSI=QIe!k0m=c3T0=UVAp6Z~Sd;2eO#7M-R&2U7ep
zHUW3hbe|2gIaYFA{>OT*>uk@yz?O2V?z*LS*?LJuHia_|ruPEI8(o{Tc6tkmSnGV=
zn(Nc+cG@t<E4huAE3i>f8r>kIJXlCUL;8ItLaqVkvD`Vxzycx;4B1+JClIS$FV_QE
zRp%`pLZ%zpns2ILA%ZG|T41PK>3|U3KV)BC0NN1Zf?ccE0U>z@gcLv`FlrTmWmKjP
z<-FX15X&l;A7==C=;xwf1mxgxA&@wEuJ=W~d9Rl%l)S6go2N{ccznlzdzNq(d5cW6
zOxG^C8U}1L&+FB@JGrPx@>$+eK7GWe`#QW;<etyv%N}<$8$B+b=@Q!+!?-S`VX#l{
zuj%5NIO$ckGq=`sdkuhtl<vyfnGCAYkc%+TBPwT#XAe1M_mI?YPq7~WWQ>JJyh~U}
z>BS<4xl3qwmE^|$a#&{9Js=Ue64#=IRYY81tm7*j#MG_7^HMm(LLz%X6;e24w}nGC
z>k8qnXJ8@Zwq8(3iiqs8PzX%jPlUO)0*35%V2I$4v<SM=h(TDl8!2ds%Z^x$!B8xm
zBUFb<T|}i2+C_qbxUa2-d=U-FC#H50b4d}A?ZH80x@@_N%a)6TcPuDMVx{7|A`;tO
z>@0d_P7!hKB~eFll{w9~5KGWaMGH{S<o4#;K6MLjq)qm)t<p6%tmb_-M&+hjdfQD7
znGnleh9k0!qre`zBnVT`hZ9!is$eu~g>~3oz`G7%t>ot5Dp~AlppkG@SRM(-)UCq$
z5#%#!@AO$*pjP3!^q1lXKoM1!0^<V~0NE@MM8E@k-%+556qpbW;w~9Rhes`fL0$v)
z{vo|fD8~T@e+ctP3qtQY%*{o57bPUK<ss(c!`yIWOwj$o874QPn~=7?h{;{*>(gz?
z!`$uTn`0pttVY9bZd8^JbIEc+xX6^p!jW)(h@ke3hV#N+6CD{1=Y*cUPdPJ0TGw(*
zvm-=W2S-LiB6FkRQ!+8zLzQ3B&92kzb=>_icWLZl?I`b(n0_qkR#EoK_M1f6{pMRm
zJ*LoiiTa_aPmB7NsPBvVyk63yoIfJ!wW5Acl<t57kzo4XEa?WKJ|=3OppZSHo`e^D
z$J)rhD0})}*3dusAQ=G3ZK5u;phykUj#}-PY(6nV-Sl$;NWS+{2G#ft`iRVDcS;G?
zVceg1;Psp8(1Q4z!bhI|`bX}(z3z6@;ny8LGQaGAk<U2iQiqS!K4gIr$|{&7YW$wV
zM?!~>wEocHBbVZ!6`yhN2%kl>@W?F=9%=oSgGW}JbnwW29F*fuhmN#A;LwpJKeXsb
zv_d!p7hV0Qfgf~tIieQS@#3d}AoRJ0C#WE3!iw)xe{iJU2Wb&H_LL9MNEiTKb%OR~
zI9~dXNP*tn`j&Cb0DN~W|0!$$d?6@z=?{}d3F^~&P@f7yeHvc%AQv_kn~D0%J{u*S
z3D-fp4t_nEop4KZ!*73ng3k>t<=aGie)sQ>C+*2fb3C_j=e4)~?=L;{*kduD2|da;
zY{4g*KkHel1XZ-u2vlgP+o68uc)yqh>wjn~ME4s41DPwQ$7iS<0`r(5Q#T{S5C&&Z
z0eiD=2&4l*Af*@i+_X#Sg+7<;EKo3nmAP`iZpiQQK>^Y@^L_h?0-NVNpDT4Kf3DAk
zx-@O4Z|~FDICFe1)unM}``oNc=~+J4>r#5Azc@=f!(Wo|r~5rA9r)Ay-i+?>?ZE-^
z5B#aVT^CupozEgL{ssONUswP{8-y!W;J5nA9J9q=?ucf8g(I3ED$yGKRgT%<uXaSe
zzs3=D5S3`P{(xiFKvbAHU!O|g&X{kHU_^mj7dV(MeDLulA~E?uikzDNj+TPSm5EaY
z6=kxPe`%66`VRq@&A|?io}oipf>_xv@a>&DDUi(kgwFTpJ7S*S<p}g9i{I%lbj&&a
zB1g>jyB)!21s#E|WbtSCJ&rlu?-d~}>2m}s;;j2tMLfrB_w8!U>dF)!YrO?1td?YS
zt8b5**f=dd%c(R@v(KT4ltx`Ly3yxwMapmR`A$Mg*ZZq88okNrT7PXuqdFO#^Vd0A
z+QYKS=7R#U-f~$Eaw^19o8+KFa?l|;(xIRKZdXbGvY-(KSV1ESn8J(^mN$|vZ~_<p
ztnqmWFo7F?DZMD*%3n$^4AMJ)#OKaq#_tN|X7v1kYkz6{c>(wSQhIKXUi>3%XHc=d
z4+V1quK%TRW(V8>OzBxc#oj&?%nUHK>sBFYX9P>KIMajF#y%rRZR>ojR*Nw3&LFj=
zcLW7iQB#9{{<rhrhE55V;cW|+^S?D%!T%Qi+o0xP72c*`HUArfHT-YjzYVGn2JqJL
z-+F5UedD1f&?g>p{I|?_M|x%jA5s+)smie<!+)dAd$jeKov))UPhr64i!``)+Qwa+
z%($KFl$eR}@I|5HGN?WeeeAsFkMmsl{NmDXtt!8xk8M*qpUppR@dw9umhNZAujW4&
zU7i1P#i`~$SN!MVKPzp#jYXg{&huF(lIgQfa`inYxiyBrvGft`r;q5>>6624Shwk>
zb(_SU>_-B`|9SsNz-!1;&tyLmU_aQem-0ggf8pG$v~}?Q_L;m_-zz_xo9({+tSuiv
zqy<iF9^^0yUjgJ(hAi>7`z<~PZ34f^espjeryKbG;UFsu7V-k)*)HwC(H9BZ%woUI
z^!Tm9o3-G`1<gL+1R**=@{l2)UF3r`2Wrr7AS~XAgOy~67}21<RM;r4ys<ug+u$5>
ztM?nt_&{&p?m@pU3)2@4$*JCFr0er)y@*iGMx-gT&|1@EgY4aRn`sur<|6O8P;l$B
z6H?BC*W&3T@_aPmnJHuAP4Omp;Uod=UYTzIpPiu3o9gMyrc^3>e~Z^ph?mg8s+soN
z7gNt-P5U?f9PH7Tqt42umpxWBi@Mf(5vO^PTA+5!%E4Jy4#iY642-BSp!UpSMOfjq
zMt%m9MiTdQp6?=~LUqPUgfF~#<6>nT8f?Bd0qYE>8*@`RKX8U2S<KA4g|XEd4!-E*
zEjwy?X_x_n?L)M!F`q3tn{LFARf$|sWa$niZ1ffsBFaOpWeyWr%P$AWzF1Z{Yw)bZ
zT&94WpR?Z}%X<LHzHg{vFKJYGpV1*&sl&WPq_R1TVrJl%s~7Fw^PIhCxL8|!&R!(b
zRJ{$Jv?sOLpvk*Ysn}ASv>TNT#;Mlg<Q=KlUYx8W4Hu^t&+bUr)EbIS!`MsC>Pzl`
zbhT<XXw7(5U%?!ARup~OtD4m)d*w5`69<-buK4V(bWxGbfPbdf#0EyLRNdVoGYl^-
zo!ya2a8H+{b6KB=%vf=5aa_zQp3Hp{@KP49EAbt+a;d5>sX^wH8eC_<@<_G!W*w<m
z)sa+-v?Ddjq-cAT+9+i<6(Q7%nu}e-Wfg>~=!0gi4^h25Z-!mtm(8w9fcj8;F{aF{
z3+*PJ%B(Jg`O;H*4HrtGE>zymC|kp)J$En<M4N|#+8{R+YZ}rIDCo<fW-Q?7$u3P=
ze!!^|{nH29u_O%zoXqiMNZJu_en+zrM|_41W*lFKePjS8tsii_Mql@Y;t+}^rD)es
zzyTG_N;o!br~O3-N<p^7(!4}`PNfVfrFiwk)zfGu_8mEVAmb2NPXMOC8oo4kz<LzZ
z^2z0JSa11tX%3c8bBu(ImS0=VrvXRw!!noCD(;0gdSniVtu}V9njM?4JuF`ys7<q1
za*P}1$mM=DlVeV!@r`tpELx+s3scYd8|3eJZhs?2(dxtpsuN?nJVdw1!ZC)<38Tx0
zXuz}~DC!(awND~WS&<pBJff+xwxZPECLM7NkXC+-&(=k-RC&uHxB1}&9IH+C*Eq()
zF7hu8Nxdv%6ocx_5l-=@X(;1}I)e=yK`8M)%@y)e_YtYS=tgX}XW~@o(Xb(0Nd20^
z1*1`TIq@hb+o#K=A_hvutK}y$?41A=o|+uev2>LhX&)huD=;2KiY`~#%cB^9dqY#2
zW={0q{I}md{pdGtJ3mKnInjLC2OfCp_yZrmdM3T0y!eB0(vY0~+D=n*{UcE2W53l(
zpNKAeg3pkbqqlx4;grw4-N+<*ahVUhM2~dSF`|{n_z8>X8?AH=Y_Oaf8dL8mVrq<z
z&7^z8cO~`7c=@g)w>_FPB@_3(>Ghu@^Y|)sKB{|CzczGApRbK(e^qaPPrWg@*O3CN
zFr8wds5h#ktc8lc->i8u_XSe%aWcaBH41$U9C`diygwE722sZizZ+5CwXyO2@+}kp
zeo=oQ>Hw+g&C|noJ((nbMhs56OcI09V&&&ZQ&tpCzDyE@!o2vHno3bP2{TC)jRX@^
z{PdqUy+27NiBL9+pP*&OmBl2%k-3XQLldN)EU-ULrttQrwr5w6Q}6Xj`P3VOf%<`X
zsx<VO4Zw%pX#4VZ4RdFO{Ht2(L-f?M15PR6$Rq(jR=};31bkgDd34q!FS$(u{pwoH
zbayCqCK-M)gkwxU{dU{8%Xc<?P{PTABJ*Xe)8$XJd{6?(*k!Xb692<QM+zCA{_(qf
z&+znzn?8^v8Mka|K83M+T-=Nqnd?#M33z8%O+UaoUjA^)?uyRDmm%xu2Y-4y+j4v4
zSc=581}gI2X7zjPe^6DvkIzx1e*K~rAKIhu+u&pJ9?iTq_UCe0?2jq8*X5MkKIMM3
zUipRJ+Ba`s`Em+O)-z==DE~PgK89q*u=-Q+56~cG(|WIb3d-3ZTX_WUxV(vaSl3tZ
zs)`A{sN6bM$%pEV&%F9$51i0h!9Ujz&2^-Y-Hoo<aMPL%Fz8YCXW>kOtDlx*_h<ip
z-n-G8$W_l|yHS-P{Tn$iB`T$*JR=uBlQ(j?{B(J(zE5ib#0IKX=a>W9pqAwn3y>N!
zzKgAxA<9#OMhhYU?hTG{dI$#g?i#PW!Nsr)t<(#7qa@>&;pIT+`SgFq6BqI|`IGB?
z-!in{czQ#HZuCyC+pcU$6!#VC3MYGw_3}x?CUc5_f=B+1UYYb>n|(8mS@-X*2RHS0
zRoCO9VokBUtEwJE7#=QJ#?!W>_-UU?MT)s%-SA1Wuwh#Iox0G`;J&AK+DZ^7-ow4P
z)BM^WMaSE0>i{@h8g3k(AoWFBit}2z=6u@8{xAp=7!~q^=71Nn2x1b`C(1RIow$r2
zfHIu6-C^)QWk$pB6^=ncIFmcdI_T+F*cgs666V6(=&3QKv78!ih>ce~6JVdCBQ+!A
zFH1wopdp-ixr^EymS0XycyHLq7haiGHB~o3@C7-op!6F&hgcg;ydp4i&9Bs%6MMdN
z?A}*hcmI=rvz7MZCyzKfTi$AF%(45EN8b4DB)Rv^Pb6QW*WKNEA~|S&B}ulKHzlXd
z#rK~WId+w~FiF;$Ur&<r<0Lsb@`}v=rR0%A`!1`?{Kpbae#a`&zma_VB|B?&o-BbV
zD*0uM5GPND$F1i&MvCL7ZthpW{mFgX2kHh+^6QKEo=n~>#e({q<cZ(8vGK<7YfdIx
z%S`L1PL3QmTmSAf6I6Wi2wkN+eQeJkSbNjJS{VOnJ^v8z(lc4lXJJtJyTJ}e_`1`F
zo`1uhG&JSiaEbibiFRm5J~1Ta%&EWUuqH7aOp`@u5J$hR0fal`&+;41Yz++de#`}a
z12q41MJ5{DjVRfl1UN*k^|4*{Z^T9R_O}7`+ZS~{>n;ry(R#h6*3|RV`J^a#bzZ!_
ziYE&}gYDm>bJ6%-I2NPz%E&QVFqEv$eD%6U-INBA+cK6vE*_q)YENDZBUM^2mpy|w
zMTPY#1Cv8LSFYFlH8vdc$yDoAd8wfBrd5iCug{zAMQim#19<Y7!s2|PtzcocI=`N?
zKy^NE*N?4PLifMLr=-(e3gAw?Dem?FJJkTZ01FP|nh_DL_YD&hCx|*dv+)-uEY`qd
z7wonT@Jwk%dxjaWB7O<$8R9Ax>x%LDRXlC18;a%gDRxCVY<-)QvYOGXBSn=6_1f82
z1jcT(rWjvvMoii@X9Cn#xmsq$rczWqzap^R*d-{9lUHO)&g<l*f=w+>3}0wVhLH9Y
zr*TtzcPcX3U2>%<!*!C6-Xi&6iebdJB6CNe5Shv<k<BGcFQ#^#t_dQu2SdRrRo&29
zoz&UNY<CIT?4lFxusdqR7-*BF?!`?FUz=aM>mA~!3!&7-K#-ss_*sqyuH~|=DPQ1g
zKa}&?;RxFx8Fw<&*Xp&Q(MGP$OpG16R5_!c+A5=q3L9m_k47%hY+!f(+T&ZQPTQi<
zur?eYxn#&$1**a#=8}4LNtX<ror-UWH?q6CL<<ATk>%FHwcyLo;<2$ifNZOy@m<jo
z39%~W<g8I*+3Kny%rCW6L(>tj))bi5f&0i)!*Mva21GQR<C#b+WWCeE@};&C_zv?a
z_UKaAHjQE1C`D0g*#aPF5o40L2%V!!U(5k4gbq>|xmS;co#81JBVgN{lB}s9RXC(l
zKEN>|fXI<cTFg&py#61aIC=ludyLumM-QL+(cRZIvGI#u`UGp#^2E*W(K`0wbyPNb
zWvtJQpIC<uM2k*xs3dy02`z~KPz%_2f@v!r*P{4ZG$5*Zl$Gqnozt6cB6R%J%j&xC
z(OUnnr5&A6(N7yG2%o-&d8?=%QIAsJiJG}x`td-{7$L#m7yD{a$Hj|@dScNZKAJqZ
z`k#{I3*!HtVq7KaDJb;!l_zn>A6)&-$->L#B@u<3Ab670(X_{?M)|?j?|pX8@o!R-
z_@`g4`EvBZj!A;Q%v+;78>LUrED|5^=A)?bbg9)@Atw|v(L71WH&v^f6!11tS^fCH
zI<0;8@7~gIOZm-Ikv7S~qMix1k*6Pmot$Bsn>q9|E@l<)JzRa6czFFcGgiDYnU<6v
z-X!I-4H^Ft?=7FYb^pHBeb`NzAl1C_cl7UN=$`XFtqP3SWqwJWU!14}OT5uN`gxZ4
z9~E+)^yA6LIvy+EYs(%rJ<P`LX~`-v>rPZ?W7dg}9KMOXCmOH4`$q|vgB~3Cf4tOh
A?EnA(

literal 0
HcmV?d00001

diff --git a/Resources/metadata-extractor-logo-square.svg b/Resources/metadata-extractor-logo-square.svg
new file mode 100644
index 0000000..4177c97
--- /dev/null
+++ b/Resources/metadata-extractor-logo-square.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   viewBox="0 0 109.88 107.61465"
+   height="109.8683"
+   width="109.8683"
+   id="svg3336"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="metadata-extractor-logo-square.svg"
+   inkscape:export-filename="C:\Users\Drew\Dev\metadata-extractor\Resources\favicon-48px.png"
+   inkscape:export-xdpi="39.319794"
+   inkscape:export-ydpi="39.319794">
+  <metadata
+     id="metadata3352">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs3350" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="958"
+     inkscape:window-height="1048"
+     id="namedview3348"
+     showgrid="false"
+     inkscape:zoom="4.5873103"
+     inkscape:cx="54.934151"
+     inkscape:cy="54.934147"
+     inkscape:window-x="-7"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg3336"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     units="px" />
+  <path
+     style="fill:#274c72"
+     inkscape:connector-curvature="0"
+     id="path3340"
+     d="m 0,50.747323 c 0,-13.81 11.21,-25 25.02,-25 1.25,1.25 15.22,15.22 17,17 l -17.05,0 -0.02,0 c -4.42,0 -8,3.58 -8,8 0,4.42 3.58,8 8,8 l 0.02,0 0,0 0.04,0 12.69,0 10.97,0 c 0,0 0,0 0,0 l 9.37,0 17,17 -26.37,0 -10.96,0 -12.69,0 -0.04,0 -0.02,0 c -4.42,0 -8,3.58 -8,8 0,4.42 3.58,8 8,8 l 0.02,0 0,0 0.05,0 23.64,0 c 0,0 0,0 0,0 l 42.36,0 17,17.000007 -83,0 c 0,0 -0.01,0 -0.02,0 -13.81,0 -25,-11.190007 -25,-25.000007 0.01,-6.08 2.23,-11.95 6.25,-16.51 -4.02,-4.56 -6.24,-10.42 -6.25,-16.49 z" />
+  <path
+     style="fill:#404041"
+     inkscape:connector-curvature="0"
+     id="path3342"
+     d="m 51.88,-1.1326776 c -13.81,0 -25,11.2100016 -25,25.0200016 1.25,1.25 15.22,15.22 17,17 l 0,-17.05 0,-0.02 c 0,-4.42 3.58,-8 8,-8 4.42,0 8,3.58 8,8 l 0,0.02 0,0 0,0.04 0,12.69 0,10.97 c 0,0 0,0 0,0 l 0,9.37 17,17 0,-26.37 0,-10.96 0,-12.69 0,-0.04 0,-0.02 c 0,-4.42 3.58,-8 8,-8 4.42,0 8,3.58 8,8 l 0,0.02 0,0 0,0.05 0,23.64 c 0,0 0,0 0,0 l 0,42.36 17,17.000006 0,-83.000006 c 0,0 0,-0.01 0,-0.02 0,-13.81 -11.19,-25.0000016 -25,-25.0000016 -6.08,0.01 -11.95,2.23 -16.51,6.25 -4.55,-4.02 -10.42,-6.24 -16.49,-6.26 z" />
+</svg>
diff --git a/Resources/metadata-extractor-logo.svg b/Resources/metadata-extractor-logo.svg
new file mode 100644
index 0000000..4ed9017
--- /dev/null
+++ b/Resources/metadata-extractor-logo.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 471.31 111.06" height="32mm" width="133mm">
+  <g transform="translate(-11.118166,-287.17383)">
+    <path d="m11.12 339.05c0-13.81 11.21-25 25.02-25 1.25 1.25 15.22 15.22 17 17l-17.05 0-0.02 0c-4.42 0-8 3.58-8 8 0 4.42 3.58 8 8 8l0.02 0 0 0 0.04 0 12.69 0 10.97 0c0 0 0 0 0 0l9.37 0 17 17-26.37 0-10.96 0-12.69 0-0.04 0-0.02 0c-4.42 0-8 3.58-8 8 0 4.42 3.58 8 8 8l0.02 0 0 0 0.05 0 23.64 0c0 0 0 0 0 0l42.36 0 17 17-83 0c0 0-0.01 0-0.02 0-13.81 0-25-11.19-25-25 0.01-6.08 2.23-11.95 6.25-16.51-4.02-4.56-6.24-10.42-6.25-16.49z" fill="#274c72"/>
+    <path d="m63 287.17c-13.81 0-25 11.21-25 25.02 1.25 1.25 15.22 15.22 17 17l0-17.05 0-0.02c0-4.42 3.58-8 8-8 4.42 0 8 3.58 8 8l0 0.02 0 0 0 0.04 0 12.69 0 10.97c0 0 0 0 0 0l0 9.37 17 17 0-26.37 0-10.96 0-12.69 0-0.04 0-0.02c0-4.42 3.58-8 8-8 4.42 0 8 3.58 8 8l0 0.02 0 0 0 0.05 0 23.64c0 0 0 0 0 0l0 42.36 17 17 0-83c0 0 0-0.01 0-0.02 0-13.81-11.19-25-25-25-6.08 0.01-11.95 2.23-16.51 6.25C74.94 289.41 69.07 287.19 63 287.17Z" fill="#404041"/>
+    <path d="m163.96 339.31-34.08 0 0-49.72 34.08 0 0 9.95-24.13 0 0 9.95 16.33 0 0 9.95-16.33 0 0 9.92 24.13 0 0 9.95zM192.85 339.31l-9.92 0 0-39.77-14.94 0 0-9.95 39.77 0 0 9.95-14.91 0 0 39.77zM240.84 319.45l0-9.95q0-2.05-0.8-3.85-0.76-1.84-2.11-3.19-1.35-1.35-3.19-2.11-1.8-0.8-3.85-0.8-2.05 0-3.88 0.8-1.8 0.76-3.16 2.11-1.35 1.35-2.15 3.19-0.76 1.8-0.76 3.85l0 9.95 19.9 0zm9.95 19.87-9.95 0 0-9.95-19.9 0 0 9.95-9.92 0 0-29.82q0-4.13 1.56-7.73 1.56-3.64 4.23-6.34 2.7-2.7 6.31-4.26 3.64-1.56 7.77-1.56 4.13 0 7.73 1.56 3.64 1.56 6.34 4.26 2.7 2.7 4.26 6.34 1.56 3.61 1.56 7.73l0 29.82zM298.62 314.45q0 3.43-0.9 6.62-0.87 3.16-2.5 5.93-1.63 2.74-3.88 5.03-2.25 2.25-5.03 3.88-2.77 1.63-5.96 2.53-3.16 0.87-6.59 0.87l-14.98 0 0-49.72 14.98 0q3.43 0 6.59 0.9 3.19 0.87 5.96 2.5 2.77 1.63 5.03 3.92 2.25 2.25 3.88 5.03 1.63 2.74 2.5 5.93 0.9 3.16 0.9 6.59zm-9.95 0q0-3.09-1.18-5.79-1.14-2.7-3.19-4.72-2.01-2.05-4.75-3.22-2.7-1.18-5.79-1.18l-4.96 0 0 29.82 4.96 0q3.09 0 5.79-1.14 2.74-1.18 4.75-3.19 2.05-2.05 3.19-4.75 1.18-2.74 1.18-5.82zM333.66 319.45l0-9.95q0-2.05-0.8-3.85-0.76-1.84-2.11-3.19-1.35-1.35-3.19-2.11-1.8-0.8-3.85-0.8-2.05 0-3.88 0.8-1.8 0.76-3.16 2.11-1.35 1.35-2.15 3.19-0.76 1.8-0.76 3.85l0 9.95 19.9 0zm9.95 19.87-9.95 0 0-9.95-19.9 0 0 9.95-9.92 0 0-29.82q0-4.13 1.56-7.73 1.56-3.64 4.23-6.34 2.7-2.7 6.31-4.26 3.64-1.56 7.77-1.56 4.13 0 7.73 1.56 3.64 1.56 6.34 4.26 2.7 2.7 4.26 6.34 1.56 3.61 1.56 7.73l0 29.82zM372.37 339.31l-9.92 0 0-39.77-14.94 0 0-9.95 39.77 0 0 9.95-14.91 0 0 39.77zM420.99 319.45l0-9.95q0-2.05-0.8-3.85-0.76-1.84-2.11-3.19-1.35-1.35-3.19-2.11-1.8-0.8-3.85-0.8-2.05 0-3.88 0.8-1.8 0.76-3.16 2.11-1.35 1.35-2.15 3.19-0.76 1.8-0.76 3.85l0 9.95 19.9 0zm9.95 19.87-9.95 0 0-9.95-19.9 0 0 9.95-9.92 0 0-29.82q0-4.13 1.56-7.73 1.56-3.64 4.23-6.34 2.7-2.7 6.31-4.26 3.64-1.56 7.77-1.56 4.13 0 7.73 1.56 3.64 1.56 6.34 4.26 2.7 2.7 4.26 6.34 1.56 3.61 1.56 7.73l0 29.82z" fill="#404041"/>
+    <path d="m171.5 397.2-11.67 0-9.25-15.52-9.29 15.52-11.6 0 14.75-24.04-14.75-24.04 11.6 0 9.29 15.52 9.25-15.52 11.67 0-14.75 24.04 14.75 24.04zM197.93 397.2l-9.59 0 0-38.45-14.45 0 0-9.62 38.45 0 0 9.62-14.42 0 0 38.45zM227.06 358.75l0 19.24 9.62 0q1.98 0 3.72-0.74 1.74-0.77 3.05-2.08 1.31-1.31 2.04-3.05 0.77-1.78 0.77-3.75 0-1.98-0.77-3.72-0.74-1.78-2.04-3.08-1.31-1.31-3.05-2.04-1.74-0.77-3.72-0.77l-9.62 0zm0 38.45-9.62 0 0-48.07 19.24 0q2.65 0 5.1 0.7 2.45 0.67 4.56 1.94 2.15 1.24 3.89 3.02 1.78 1.74 3.02 3.89 1.27 2.15 1.94 4.59 0.7 2.45 0.7 5.1 0 2.48-0.64 4.83-0.6 2.35-1.78 4.46-1.14 2.11-2.82 3.89-1.68 1.78-3.75 3.08l5.33 12.57-10.22 0-4.19-9.69-10.76 0.07 0 9.62zM290.17 377.99l0-9.62q0-1.98-0.77-3.72-0.74-1.78-2.04-3.08-1.31-1.31-3.08-2.04-1.74-0.77-3.72-0.77-1.98 0-3.75 0.77-1.74 0.74-3.05 2.04-1.31 1.31-2.08 3.08-0.74 1.74-0.74 3.72l0 9.62 19.24 0zm9.62 19.21-9.62 0 0-9.62-19.24 0 0 9.62-9.59 0 0-28.83q0-3.99 1.51-7.48 1.51-3.52 4.09-6.13 2.61-2.61 6.1-4.12 3.52-1.51 7.51-1.51 3.99 0 7.48 1.51 3.52 1.51 6.13 4.12 2.61 2.61 4.12 6.13 1.51 3.49 1.51 7.48l0 28.83zM345.47 392.27q-3.35 2.88-7.51 4.43-4.16 1.54-8.62 1.54-3.42 0-6.6-0.91-3.15-0.87-5.93-2.48-2.75-1.64-5.03-3.92-2.28-2.28-3.92-5.03-1.61-2.78-2.51-5.93-0.87-3.18-0.87-6.6 0-3.42 0.87-6.6 0.91-3.18 2.51-5.93 1.64-2.78 3.92-5.06 2.28-2.28 5.03-3.89 2.78-1.64 5.93-2.51 3.18-0.91 6.6-0.91 4.46 0 8.62 1.54 4.16 1.51 7.51 4.43l-5.1 8.38q-2.11-2.28-4.99-3.49-2.88-1.24-6.03-1.24-3.18 0-5.97 1.21-2.78 1.21-4.86 3.29-2.08 2.04-3.29 4.86-1.21 2.78-1.21 5.93 0 3.15 1.21 5.93 1.21 2.75 3.29 4.83 2.08 2.08 4.86 3.29 2.78 1.21 5.97 1.21 3.15 0 6.03-1.21 2.88-1.24 4.99-3.52l5.1 8.38zM373.01 397.2l-9.59 0 0-38.45-14.45 0 0-9.62 38.45 0 0 9.62-14.42 0 0 38.45zM437.51 373.36q0 3.42-0.91 6.6-0.87 3.15-2.48 5.93-1.61 2.75-3.89 5.03-2.28 2.28-5.03 3.92-2.75 1.61-5.93 2.48-3.18 0.91-6.6 0.91-3.42 0-6.6-0.91-3.15-0.87-5.93-2.48-2.75-1.64-5.03-3.92-2.28-2.28-3.92-5.03-1.61-2.78-2.51-5.93-0.87-3.18-0.87-6.6 0-3.42 0.87-6.6 0.91-3.18 2.51-5.93 1.64-2.75 3.92-5.03 2.28-2.28 5.03-3.89 2.78-1.61 5.93-2.48 3.18-0.91 6.6-0.91 3.42 0 6.6 0.91 3.18 0.87 5.93 2.48 2.75 1.61 5.03 3.89 2.28 2.28 3.89 5.03 1.61 2.75 2.48 5.93 0.91 3.18 0.91 6.6zm-9.55 0q0-3.15-1.21-5.93-1.21-2.82-3.29-4.86-2.04-2.08-4.86-3.29-2.78-1.21-5.93-1.21-3.18 0-5.97 1.21-2.78 1.21-4.86 3.29-2.08 2.04-3.29 4.86-1.21 2.78-1.21 5.93 0 3.15 1.21 5.93 1.21 2.75 3.29 4.83 2.08 2.08 4.86 3.29 2.78 1.21 5.97 1.21 3.15 0 5.93-1.21 2.82-1.21 4.86-3.29 2.08-2.08 3.29-4.83 1.21-2.78 1.21-5.93zM453.6 358.75l0 19.24 9.62 0q1.98 0 3.72-0.74 1.74-0.77 3.05-2.08 1.31-1.31 2.04-3.05 0.77-1.78 0.77-3.75 0-1.98-0.77-3.72-0.74-1.78-2.04-3.08-1.31-1.31-3.05-2.04-1.74-0.77-3.72-0.77l-9.62 0zm0 38.45-9.62 0 0-48.07 19.24 0q2.65 0 5.1 0.7 2.45 0.67 4.56 1.94 2.15 1.24 3.89 3.02 1.78 1.74 3.02 3.89 1.27 2.15 1.94 4.59 0.7 2.45 0.7 5.1 0 2.48-0.64 4.83-0.6 2.35-1.78 4.46-1.14 2.11-2.82 3.89-1.68 1.78-3.75 3.08l5.33 12.57-10.22 0-4.19-9.69-10.76 0.07 0 9.62z" fill="#274c72"/>
+  </g>
+</svg>
diff --git a/Samples/com/drew/metadata/GeoTagMapBuilder.java b/Samples/com/drew/metadata/GeoTagMapBuilder.java
index 769bbb6..cd4de5a 100644
--- a/Samples/com/drew/metadata/GeoTagMapBuilder.java
+++ b/Samples/com/drew/metadata/GeoTagMapBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -66,21 +66,28 @@ public class GeoTagMapBuilder
             }
         });
 
+        if (files == null)
+        {
+            System.err.println("No matching files found.");
+            System.exit(1);
+        }
+
         Collection<PhotoLocation> photoLocations = new ArrayList<PhotoLocation>();
         for (File file : files)
         {
             // Read all metadata from the image
             Metadata metadata = ImageMetadataReader.readMetadata(file);
             // See whether it has GPS data
-            GpsDirectory gpsDirectory = metadata.getDirectory(GpsDirectory.class);
-            if (gpsDirectory == null)
-                continue;
-            // Try to read out the location, making sure it's non-zero
-            GeoLocation geoLocation = gpsDirectory.getGeoLocation();
-            if (geoLocation == null || geoLocation.isZero())
-                continue;
-            // Add to our collection for use below
-            photoLocations.add(new PhotoLocation(geoLocation, file));
+            Collection<GpsDirectory> gpsDirectories = metadata.getDirectoriesOfType(GpsDirectory.class);
+            for (GpsDirectory gpsDirectory : gpsDirectories) {
+                // Try to read out the location, making sure it's non-zero
+                GeoLocation geoLocation = gpsDirectory.getGeoLocation();
+                if (geoLocation != null && !geoLocation.isZero()) {
+                    // Add to our collection for use below
+                    photoLocations.add(new PhotoLocation(geoLocation, file));
+                    break;
+                }
+            }
         }
 
         // Write output to the console.
diff --git a/Samples/com/drew/metadata/SampleUsage.java b/Samples/com/drew/metadata/SampleUsage.java
index db52f0b..74e295a 100644
--- a/Samples/com/drew/metadata/SampleUsage.java
+++ b/Samples/com/drew/metadata/SampleUsage.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -34,7 +34,7 @@ import java.util.Arrays;
 
 /**
  * Showcases the most popular ways of using the metadata-extractor library.
- * <p/>
+ * <p>
  * For more information, see the project wiki: https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted
  *
  * @author Drew Noakes https://drewnoakes.com
diff --git a/Samples/com/drew/metadata/XmpSample.java b/Samples/com/drew/metadata/XmpSample.java
new file mode 100644
index 0000000..f52459c
--- /dev/null
+++ b/Samples/com/drew/metadata/XmpSample.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPIterator;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.properties.XMPPropertyInfo;
+import com.drew.imaging.ImageMetadataReader;
+import com.drew.imaging.ImageProcessingException;
+import com.drew.metadata.xmp.XmpDirectory;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Shows basic extraction and iteration of XMP data.
+ * <p>
+ * For more information, see the project wiki: https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class XmpSample
+{
+    private static void xmpSample(InputStream imageStream) throws XMPException, ImageProcessingException, IOException
+    {
+        // Extract metadata from the image
+        Metadata metadata = ImageMetadataReader.readMetadata(imageStream);
+
+        // Iterate through any XMP directories we may have received
+        for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) {
+
+            // Usually with metadata-extractor, you iterate a directory's tags. However XMP has
+            // a complex structure with many potentially unknown properties. This doesn't map
+            // well to metadata-extractor's directory-and-tag model.
+            //
+            // If you need to use XMP data, access the XMPMeta object directly.
+            XMPMeta xmpMeta = xmpDirectory.getXMPMeta();
+
+            XMPIterator itr = xmpMeta.iterator();
+
+            // Iterate XMP properties
+            while (itr.hasNext()) {
+
+                XMPPropertyInfo property = (XMPPropertyInfo) itr.next();
+
+                // Print details of the property
+                System.out.println(property.getPath() + ": " + property.getValue());
+            }
+        }
+    }
+}
diff --git a/Source/com/drew/imaging/FileType.java b/Source/com/drew/imaging/FileType.java
index c9b2796..07e6f13 100644
--- a/Source/com/drew/imaging/FileType.java
+++ b/Source/com/drew/imaging/FileType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -33,6 +33,8 @@ public enum FileType
     Bmp,
     Gif,
     Ico,
+    Pcx,
+    Riff,
 
     /** Sony camera raw. */
     Arw,
diff --git a/Source/com/drew/imaging/FileTypeDetector.java b/Source/com/drew/imaging/FileTypeDetector.java
index 2e379c3..d5a7b83 100644
--- a/Source/com/drew/imaging/FileTypeDetector.java
+++ b/Source/com/drew/imaging/FileTypeDetector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -49,12 +49,19 @@ public class FileTypeDetector
         _root.addPath(FileType.Gif, "GIF87a".getBytes());
         _root.addPath(FileType.Gif, "GIF89a".getBytes());
         _root.addPath(FileType.Ico, new byte[]{0x00, 0x00, 0x01, 0x00});
+        _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x00, 0x01}); // multiple PCX versions, explicitly listed
+        _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x02, 0x01});
+        _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x03, 0x01});
+        _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x05, 0x01});
+        _root.addPath(FileType.Riff, "RIFF".getBytes());
 
         _root.addPath(FileType.Arw, "II".getBytes(), new byte[]{0x2a, 0x00, 0x08, 0x00});
         _root.addPath(FileType.Crw, "II".getBytes(), new byte[]{0x1a, 0x00, 0x00, 0x00}, "HEAPCCDR".getBytes());
         _root.addPath(FileType.Cr2, "II".getBytes(), new byte[]{0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52});
         _root.addPath(FileType.Nef, "MM".getBytes(), new byte[]{0x00, 0x2a, 0x00, 0x00, 0x00, (byte)0x80, 0x00});
         _root.addPath(FileType.Orf, "IIRO".getBytes(), new byte[]{(byte)0x08, 0x00});
+        _root.addPath(FileType.Orf, "MMOR".getBytes(), new byte[]{(byte)0x00, 0x00});
+        _root.addPath(FileType.Orf, "IIRS".getBytes(), new byte[]{(byte)0x08, 0x00});
         _root.addPath(FileType.Raf, "FUJIFILMCCD-RAW".getBytes());
         _root.addPath(FileType.Rw2, "II".getBytes(), new byte[]{0x55, 0x00});
     }
@@ -77,6 +84,9 @@ public class FileTypeDetector
     @NotNull
     public static FileType detectFileType(@NotNull final BufferedInputStream inputStream) throws IOException
     {
+        if (!inputStream.markSupported())
+            throw new IOException("Stream must support mark/reset");
+
         int maxByteCount = _root.getMaxDepth();
 
         inputStream.mark(maxByteCount);
diff --git a/Source/com/drew/imaging/ImageMetadataReader.java b/Source/com/drew/imaging/ImageMetadataReader.java
index 5461be2..1d154a7 100644
--- a/Source/com/drew/imaging/ImageMetadataReader.java
+++ b/Source/com/drew/imaging/ImageMetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,10 +22,15 @@ package com.drew.imaging;
 
 import com.drew.imaging.bmp.BmpMetadataReader;
 import com.drew.imaging.gif.GifMetadataReader;
+import com.drew.imaging.ico.IcoMetadataReader;
 import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.imaging.pcx.PcxMetadataReader;
 import com.drew.imaging.png.PngMetadataReader;
 import com.drew.imaging.psd.PsdMetadataReader;
+import com.drew.imaging.raf.RafMetadataReader;
 import com.drew.imaging.tiff.TiffMetadataReader;
+import com.drew.imaging.webp.WebpMetadataReader;
+import com.drew.lang.RandomAccessStreamReader;
 import com.drew.lang.StringUtil;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
@@ -33,7 +38,7 @@ import com.drew.metadata.Metadata;
 import com.drew.metadata.MetadataException;
 import com.drew.metadata.Tag;
 import com.drew.metadata.exif.ExifIFD0Directory;
-import com.drew.metadata.exif.ExifThumbnailDirectory;
+import com.drew.metadata.file.FileMetadataReader;
 
 import java.io.*;
 import java.util.ArrayList;
@@ -41,17 +46,25 @@ import java.util.Arrays;
 import java.util.Collection;
 
 /**
- * Obtains {@link Metadata} from all supported file formats.
+ * Reads metadata from any supported file format.
  * <p>
- * This class a lightweight wrapper around specific file type processors:
+ * This class a lightweight wrapper around other, specific metadata processors.
+ * During extraction, the file type is determined from the first few bytes of the file.
+ * Parsing is then delegated to one of:
+ *
  * <ul>
  *     <li>{@link JpegMetadataReader} for JPEG files</li>
  *     <li>{@link TiffMetadataReader} for TIFF and (most) RAW files</li>
  *     <li>{@link PsdMetadataReader} for Photoshop files</li>
- *     <li>{@link PngMetadataReader} for BMP files</li>
+ *     <li>{@link PngMetadataReader} for PNG files</li>
  *     <li>{@link BmpMetadataReader} for BMP files</li>
  *     <li>{@link GifMetadataReader} for GIF files</li>
+ *     <li>{@link IcoMetadataReader} for ICO files</li>
+ *     <li>{@link PcxMetadataReader} for PCX files</li>
+ *     <li>{@link WebpMetadataReader} for WebP files</li>
+ *     <li>{@link RafMetadataReader} for RAF files</li>
  * </ul>
+ *
  * If you know the file type you're working with, you may use one of the above processors directly.
  * For most scenarios it is simpler, more convenient and more robust to use this class.
  * <p>
@@ -64,17 +77,6 @@ public class ImageMetadataReader
 {
     /**
      * Reads metadata from an {@link InputStream}.
-     * <p>
-     * The file type is determined by inspecting the leading bytes of the stream, and parsing of the file
-     * is delegated to one of:
-     * <ul>
-     *     <li>{@link JpegMetadataReader} for JPEG files</li>
-     *     <li>{@link TiffMetadataReader} for TIFF and (most) RAW files</li>
-     *     <li>{@link PsdMetadataReader} for Photoshop files</li>
-     *     <li>{@link PngMetadataReader} for PNG files</li>
-     *     <li>{@link BmpMetadataReader} for BMP files</li>
-     *     <li>{@link GifMetadataReader} for GIF files</li>
-     * </ul>
      *
      * @param inputStream a stream from which the file data may be read.  The stream must be positioned at the
      *                    beginning of the file's data.
@@ -83,6 +85,21 @@ public class ImageMetadataReader
      */
     @NotNull
     public static Metadata readMetadata(@NotNull final InputStream inputStream) throws ImageProcessingException, IOException
+    {
+        return readMetadata(inputStream, -1);
+    }
+
+    /**
+     * Reads metadata from an {@link InputStream} of known length.
+     *
+     * @param inputStream a stream from which the file data may be read.  The stream must be positioned at the
+     *                    beginning of the file's data.
+     * @param streamLength the length of the stream, if known, otherwise -1.
+     * @return a populated {@link Metadata} object containing directories of tags with values and any processing errors.
+     * @throws ImageProcessingException if the file type is unknown, or for general processing errors.
+     */
+    @NotNull
+    public static Metadata readMetadata(@NotNull final InputStream inputStream, final long streamLength) throws ImageProcessingException, IOException
     {
         BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream
             ? (BufferedInputStream)inputStream
@@ -99,7 +116,7 @@ public class ImageMetadataReader
             fileType == FileType.Nef ||
             fileType == FileType.Orf ||
             fileType == FileType.Rw2)
-            return TiffMetadataReader.readMetadata(bufferedInputStream);
+            return TiffMetadataReader.readMetadata(new RandomAccessStreamReader(bufferedInputStream, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, streamLength));
 
         if (fileType == FileType.Psd)
             return PsdMetadataReader.readMetadata(bufferedInputStream);
@@ -113,22 +130,23 @@ public class ImageMetadataReader
         if (fileType == FileType.Gif)
             return GifMetadataReader.readMetadata(bufferedInputStream);
 
+        if (fileType == FileType.Ico)
+            return IcoMetadataReader.readMetadata(bufferedInputStream);
+
+        if (fileType == FileType.Pcx)
+            return PcxMetadataReader.readMetadata(bufferedInputStream);
+
+        if (fileType == FileType.Riff)
+            return WebpMetadataReader.readMetadata(bufferedInputStream);
+
+        if (fileType == FileType.Raf)
+            return RafMetadataReader.readMetadata(bufferedInputStream);
+
         throw new ImageProcessingException("File format is not supported");
     }
 
     /**
      * Reads {@link Metadata} from a {@link File} object.
-     * <p>
-     * The file type is determined by inspecting the leading bytes of the stream, and parsing of the file
-     * is delegated to one of:
-     * <ul>
-     *     <li>{@link JpegMetadataReader} for JPEG files</li>
-     *     <li>{@link TiffMetadataReader} for TIFF and (most) RAW files</li>
-     *     <li>{@link PsdMetadataReader} for Photoshop files</li>
-     *     <li>{@link PngMetadataReader} for PNG files</li>
-     *     <li>{@link BmpMetadataReader} for BMP files</li>
-     *     <li>{@link GifMetadataReader} for GIF files</li>
-     * </ul>
      *
      * @param file a file from which the image data may be read.
      * @return a populated {@link Metadata} object containing directories of tags with values and any processing errors.
@@ -138,11 +156,14 @@ public class ImageMetadataReader
     public static Metadata readMetadata(@NotNull final File file) throws ImageProcessingException, IOException
     {
         InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
         try {
-            return readMetadata(inputStream);
+            metadata = readMetadata(inputStream, file.length());
         } finally {
             inputStream.close();
         }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
     }
 
     private ImageMetadataReader() throws Exception
@@ -166,7 +187,6 @@ public class ImageMetadataReader
     public static void main(@NotNull String[] args) throws MetadataException, IOException
     {
         Collection<String> argList = new ArrayList<String>(Arrays.asList(args));
-        boolean thumbRequested = argList.remove("-thumb");
         boolean markdownFormat = argList.remove("-markdown");
         boolean showHex = argList.remove("-hex");
 
@@ -183,7 +203,7 @@ public class ImageMetadataReader
             File file = new File(filePath);
 
             if (!markdownFormat && argList.size()>1)
-                System.out.printf("\n***** PROCESSING: %s\n%n", filePath);
+                System.out.printf("\n***** PROCESSING: %s%n%n", filePath);
 
             Metadata metadata = null;
             try {
@@ -198,8 +218,8 @@ public class ImageMetadataReader
 
             if (markdownFormat) {
                 String fileName = file.getName();
-                String urlName = StringUtil.urlEncode(fileName);
-                ExifIFD0Directory exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class);
+                String urlName = StringUtil.urlEncode(filePath);
+                ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
                 String make = exifIFD0Directory == null ? "" : exifIFD0Directory.getString(ExifIFD0Directory.TAG_MAKE);
                 String model = exifIFD0Directory == null ? "" : exifIFD0Directory.getString(ExifIFD0Directory.TAG_MODEL);
                 System.out.println();
@@ -234,9 +254,8 @@ public class ImageMetadataReader
                                 Integer.toHexString(tag.getTagType()),
                                 tagName,
                                 description);
-                    }
-                    else
-                    {
+                    } else {
+                        // simple formatting
                         if (showHex) {
                             System.out.printf("[%s - %s] %s = %s%n", directoryName, tag.getTagTypeHex(), tagName, description);
                         } else {
@@ -249,16 +268,6 @@ public class ImageMetadataReader
                 for (String error : directory.getErrors())
                     System.err.println("ERROR: " + error);
             }
-
-            if (args.length > 1 && thumbRequested) {
-                ExifThumbnailDirectory directory = metadata.getDirectory(ExifThumbnailDirectory.class);
-                if (directory!=null && directory.hasThumbnailData()) {
-                    System.out.println("Writing thumbnail...");
-                    directory.writeThumbnail(args[0].trim() + ".thumb.jpg");
-                } else {
-                    System.out.println("No thumbnail data exists in this image");
-                }
-            }
         }
     }
 }
diff --git a/Source/com/drew/imaging/ImageProcessingException.java b/Source/com/drew/imaging/ImageProcessingException.java
index ebfa440..0d15eba 100644
--- a/Source/com/drew/imaging/ImageProcessingException.java
+++ b/Source/com/drew/imaging/ImageProcessingException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/PhotographicConversions.java b/Source/com/drew/imaging/PhotographicConversions.java
index 913acac..19a3a5c 100644
--- a/Source/com/drew/imaging/PhotographicConversions.java
+++ b/Source/com/drew/imaging/PhotographicConversions.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/bmp/BmpMetadataReader.java b/Source/com/drew/imaging/bmp/BmpMetadataReader.java
index d672283..9c94ca8 100644
--- a/Source/com/drew/imaging/bmp/BmpMetadataReader.java
+++ b/Source/com/drew/imaging/bmp/BmpMetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/bmp/package-info.java b/Source/com/drew/imaging/bmp/package-info.java
new file mode 100644
index 0000000..eabaca6
--- /dev/null
+++ b/Source/com/drew/imaging/bmp/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for working with BMP files.
+ *
+ * @since 2.7.0
+ */
+package com.drew.imaging.bmp;
diff --git a/Source/com/drew/imaging/bmp/package.html b/Source/com/drew/imaging/bmp/package.html
deleted file mode 100644
index 8a26207..0000000
--- a/Source/com/drew/imaging/bmp/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with BMP files.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
diff --git a/Source/com/drew/imaging/gif/GifMetadataReader.java b/Source/com/drew/imaging/gif/GifMetadataReader.java
index b2a09b6..964d6b0 100644
--- a/Source/com/drew/imaging/gif/GifMetadataReader.java
+++ b/Source/com/drew/imaging/gif/GifMetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ package com.drew.imaging.gif;
 import com.drew.lang.StreamReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.file.FileMetadataReader;
 import com.drew.metadata.gif.GifReader;
 
 import java.io.File;
@@ -41,15 +42,15 @@ public class GifMetadataReader
     @NotNull
     public static Metadata readMetadata(@NotNull File file) throws IOException
     {
-        FileInputStream stream = null;
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
         try {
-            stream = new FileInputStream(file);
-            return readMetadata(stream);
+            metadata = readMetadata(inputStream);
         } finally {
-            if (stream != null) {
-                stream.close();
-            }
+            inputStream.close();
         }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
     }
 
     @NotNull
diff --git a/Source/com/drew/imaging/gif/package-info.java b/Source/com/drew/imaging/gif/package-info.java
new file mode 100644
index 0000000..2004e18
--- /dev/null
+++ b/Source/com/drew/imaging/gif/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with GIF files.
+ */
+package com.drew.imaging.gif;
diff --git a/Source/com/drew/imaging/gif/package.html b/Source/com/drew/imaging/gif/package.html
deleted file mode 100644
index 8e1f885..0000000
--- a/Source/com/drew/imaging/gif/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with GIF files.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/imaging/ico/IcoMetadataReader.java b/Source/com/drew/imaging/ico/IcoMetadataReader.java
new file mode 100644
index 0000000..8372a73
--- /dev/null
+++ b/Source/com/drew/imaging/ico/IcoMetadataReader.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.ico;
+
+import com.drew.lang.StreamReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.file.FileMetadataReader;
+import com.drew.metadata.ico.IcoReader;
+
+import java.io.*;
+
+/**
+ * Obtains metadata from ICO (Windows Icon) files.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class IcoMetadataReader
+{
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file) throws IOException
+    {
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
+        try {
+            metadata = readMetadata(inputStream);
+        } finally {
+            inputStream.close();
+        }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream)
+    {
+        Metadata metadata = new Metadata();
+        new IcoReader().extract(new StreamReader(inputStream), metadata);
+        return metadata;
+    }
+}
diff --git a/Source/com/drew/imaging/ico/package-info.java b/Source/com/drew/imaging/ico/package-info.java
new file mode 100644
index 0000000..762b1c2
--- /dev/null
+++ b/Source/com/drew/imaging/ico/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with ICO (Windows Icon) files.
+ */
+package com.drew.imaging.ico;
diff --git a/Source/com/drew/imaging/jpeg/JpegMetadataReader.java b/Source/com/drew/imaging/jpeg/JpegMetadataReader.java
index 20e3f7f..e4f0918 100644
--- a/Source/com/drew/imaging/jpeg/JpegMetadataReader.java
+++ b/Source/com/drew/imaging/jpeg/JpegMetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -26,11 +26,16 @@ 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.jfxx.JfxxReader;
 import com.drew.metadata.jpeg.JpegCommentReader;
+import com.drew.metadata.jpeg.JpegDhtReader;
+import com.drew.metadata.jpeg.JpegDnlReader;
 import com.drew.metadata.jpeg.JpegReader;
+import com.drew.metadata.photoshop.DuckyReader;
 import com.drew.metadata.photoshop.PhotoshopReader;
 import com.drew.metadata.xmp.XmpReader;
 
@@ -53,12 +58,16 @@ public class JpegMetadataReader
             new JpegReader(),
             new JpegCommentReader(),
             new JfifReader(),
+            new JfxxReader(),
             new ExifReader(),
             new XmpReader(),
             new IccReader(),
             new PhotoshopReader(),
+            new DuckyReader(),
             new IptcReader(),
-            new AdobeJpegReader()
+            new AdobeJpegReader(),
+            new JpegDhtReader(),
+            new JpegDnlReader()
     );
 
     @NotNull
@@ -78,15 +87,15 @@ public class JpegMetadataReader
     @NotNull
     public static Metadata readMetadata(@NotNull File file, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException
     {
-        InputStream inputStream = null;
-        try
-        {
-            inputStream = new FileInputStream(file);
-            return readMetadata(inputStream, readers);
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
+        try {
+            metadata = readMetadata(inputStream, readers);
         } finally {
-            if (inputStream != null)
-                inputStream.close();
+            inputStream.close();
         }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
     }
 
     @NotNull
@@ -122,11 +131,7 @@ public class JpegMetadataReader
         // Pass the appropriate byte arrays to each reader.
         for (JpegSegmentMetadataReader reader : readers) {
             for (JpegSegmentType segmentType : reader.getSegmentTypes()) {
-                for (byte[] segmentBytes : segmentData.getSegments(segmentType)) {
-                    if (reader.canProcess(segmentBytes, segmentType)) {
-                        reader.extract(segmentBytes, metadata, segmentType);
-                    }
-                }
+                reader.readJpegSegments(segmentData.getSegments(segmentType), metadata, segmentType);
             }
         }
     }
diff --git a/Source/com/drew/imaging/jpeg/JpegProcessingException.java b/Source/com/drew/imaging/jpeg/JpegProcessingException.java
index 78fd61a..61e2b09 100644
--- a/Source/com/drew/imaging/jpeg/JpegProcessingException.java
+++ b/Source/com/drew/imaging/jpeg/JpegProcessingException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentData.java b/Source/com/drew/imaging/jpeg/JpegSegmentData.java
index 70695e5..11c367e 100644
--- a/Source/com/drew/imaging/jpeg/JpegSegmentData.java
+++ b/Source/com/drew/imaging/jpeg/JpegSegmentData.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java b/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java
index eca8a3a..e1d2aef 100644
--- a/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java
+++ b/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java
@@ -12,21 +12,15 @@ public interface JpegSegmentMetadataReader
      * Gets the set of JPEG segment types that this reader is interested in.
      */
     @NotNull
-    public Iterable<JpegSegmentType> getSegmentTypes();
+    Iterable<JpegSegmentType> getSegmentTypes();
 
     /**
-     * Gets a value indicating whether the supplied byte data can be processed by this reader. This is not a guarantee
-     * that no errors will occur, but rather a best-effort indication of whether the parse is likely to succeed.
-     * Implementations are expected to check things such as the opening bytes, data length, etc.
-     */
-    public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType);
-
-    /**
-     * Extracts metadata from a JPEG segment's byte array and merges it into the specified {@link Metadata} object.
+     * Extracts metadata from all instances of a particular JPEG segment type.
      *
-     * @param segmentBytes The byte array from which the metadata should be extracted.
+     * @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 extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType);
+    void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType);
 }
diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentReader.java b/Source/com/drew/imaging/jpeg/JpegSegmentReader.java
index d8e6876..a6b0b5c 100644
--- a/Source/com/drew/imaging/jpeg/JpegSegmentReader.java
+++ b/Source/com/drew/imaging/jpeg/JpegSegmentReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -42,6 +42,11 @@ import java.util.Set;
  */
 public class JpegSegmentReader
 {
+    /**
+     * The 0xFF byte that signals the start of a segment.
+     */
+    private static final byte SEGMENT_IDENTIFIER = (byte) 0xFF;
+
     /**
      * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet).
      */
@@ -111,19 +116,14 @@ public class JpegSegmentReader
             // 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 segmentIdentifier = reader.getInt8();
             byte segmentType = reader.getInt8();
-            while (segmentType == (byte)0xFF)
-                segmentType = reader.getInt8();
 
-            if (segmentType == 0)
-                throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier");
+            // Read until we have a 0xFF byte followed by a byte that is not 0xFF or 0x00
+            while (segmentIdentifier != SEGMENT_IDENTIFIER || segmentType == SEGMENT_IDENTIFIER || segmentType == 0) {
+            	segmentIdentifier = segmentType;
+            	segmentType = reader.getInt8();
+            }
 
             if (segmentType == SEGMENT_SOS) {
                 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentType.java b/Source/com/drew/imaging/jpeg/JpegSegmentType.java
index d76414e..521e478 100644
--- a/Source/com/drew/imaging/jpeg/JpegSegmentType.java
+++ b/Source/com/drew/imaging/jpeg/JpegSegmentType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,17 +29,22 @@ import java.util.List;
 /**
  * An enumeration of the known segment types found in JPEG files.
  *
+ * <ul>
+ *     <li>http://www.ozhiker.com/electronics/pjmt/jpeg_info/app_segments.html</li>
+ *     <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html</li>
+ * </ul>
+ *
  * @author Drew Noakes https://drewnoakes.com
  */
 public enum JpegSegmentType
 {
-    /** APP0 JPEG segment identifier -- JFIF data (also JFXX apparently). */
+    /** APP0 JPEG segment identifier. Commonly contains JFIF, JFXX. */
     APP0((byte)0xE0, true),
 
-    /** APP1 JPEG segment identifier -- where Exif data is kept.  XMP data is also kept in here, though usually in a second instance. */
+    /** APP1 JPEG segment identifier. Commonly contains Exif. XMP data is also kept in here, though usually in a second instance. */
     APP1((byte)0xE1, true),
 
-    /** APP2 JPEG segment identifier. */
+        /** APP2 JPEG segment identifier. Commonly contains ICC. */
     APP2((byte)0xE2, true),
 
     /** APP3 JPEG segment identifier. */
@@ -63,7 +68,7 @@ public enum JpegSegmentType
     /** APP9 JPEG segment identifier. */
     APP9((byte)0xE9, true),
 
-    /** APPA (App10) JPEG segment identifier -- can hold Unicode comments. */
+    /** APPA (App10) JPEG segment identifier. Can contain Unicode comments, though {@link JpegSegmentType#COM} is more commonly used for comments. */
     APPA((byte)0xEA, true),
 
     /** APPB (App11) JPEG segment identifier. */
@@ -72,10 +77,10 @@ public enum JpegSegmentType
     /** APPC (App12) JPEG segment identifier. */
     APPC((byte)0xEC, true),
 
-    /** APPD (App13) JPEG segment identifier -- IPTC data in here. */
+    /** APPD (App13) JPEG segment identifier. Commonly contains IPTC, Photoshop data. */
     APPD((byte)0xED, true),
 
-    /** APPE (App14) JPEG segment identifier. */
+    /** APPE (App14) JPEG segment identifier. Commonly contains Adobe data. */
     APPE((byte)0xEE, true),
 
     /** APPF (App15) JPEG segment identifier. */
@@ -87,58 +92,73 @@ public enum JpegSegmentType
     /** Define Quantization Table segment identifier. */
     DQT((byte)0xDB, false),
 
+    /** Define Number of Lines segment identifier. */
+    DNL((byte)0xDC, false),
+
+    /** Define Restart Interval segment identifier. */
+    DRI((byte)0xDD, false),
+
+    /** Define Hierarchical Progression segment identifier. */
+    DHP((byte)0xDE, false),
+
+    /** EXPand reference component(s) segment identifier. */
+    EXP((byte)0xDF, false),
+
     /** Define Huffman Table segment identifier. */
     DHT((byte)0xC4, false),
 
-    /** Start-of-Frame (0) segment identifier. */
+    /** Define Arithmetic Coding conditioning segment identifier. */
+    DAC((byte)0xCC, false),
+
+    /** Start-of-Frame (0) segment identifier for Baseline DCT. */
     SOF0((byte)0xC0, true),
 
-    /** Start-of-Frame (1) segment identifier. */
+    /** Start-of-Frame (1) segment identifier for Extended sequential DCT. */
     SOF1((byte)0xC1, true),
 
-    /** Start-of-Frame (2) segment identifier. */
+    /** Start-of-Frame (2) segment identifier for Progressive DCT. */
     SOF2((byte)0xC2, true),
 
-    /** Start-of-Frame (3) segment identifier. */
+    /** Start-of-Frame (3) segment identifier for Lossless (sequential). */
     SOF3((byte)0xC3, true),
 
 //    /** Start-of-Frame (4) segment identifier. */
 //    SOF4((byte)0xC4, true),
 
-    /** Start-of-Frame (5) segment identifier. */
+    /** Start-of-Frame (5) segment identifier for Differential sequential DCT. */
     SOF5((byte)0xC5, true),
 
-    /** Start-of-Frame (6) segment identifier. */
+    /** Start-of-Frame (6) segment identifier for Differential progressive DCT. */
     SOF6((byte)0xC6, true),
 
-    /** Start-of-Frame (7) segment identifier. */
+    /** Start-of-Frame (7) segment identifier for Differential lossless (sequential). */
     SOF7((byte)0xC7, true),
 
-    /** Start-of-Frame (8) segment identifier. */
-    SOF8((byte)0xC8, true),
+    /** Reserved for JPEG extensions. */
+    JPG((byte)0xC8, true),
 
-    /** Start-of-Frame (9) segment identifier. */
+    /** Start-of-Frame (9) segment identifier for Extended sequential DCT. */
     SOF9((byte)0xC9, true),
 
-    /** Start-of-Frame (10) segment identifier. */
+    /** Start-of-Frame (10) segment identifier for Progressive DCT. */
     SOF10((byte)0xCA, true),
 
-    /** Start-of-Frame (11) segment identifier. */
+    /** Start-of-Frame (11) segment identifier for Lossless (sequential). */
     SOF11((byte)0xCB, true),
 
 //    /** Start-of-Frame (12) segment identifier. */
 //    SOF12((byte)0xCC, true),
 
-    /** Start-of-Frame (13) segment identifier. */
+    /** Start-of-Frame (13) segment identifier for Differential sequential DCT. */
     SOF13((byte)0xCD, true),
 
-    /** Start-of-Frame (14) segment identifier. */
+    /** Start-of-Frame (14) segment identifier for Differential progressive DCT. */
     SOF14((byte)0xCE, true),
 
-    /** Start-of-Frame (15) segment identifier. */
+    /** Start-of-Frame (15) segment identifier for Differential lossless (sequential). */
     SOF15((byte)0xCF, true),
 
-    /** JPEG comment segment identifier. */
+    /** JPEG comment segment identifier for comments. */
     COM((byte)0xFE, true);
 
     public static final Collection<JpegSegmentType> canContainMetadataTypes;
diff --git a/Source/com/drew/imaging/jpeg/package-info.java b/Source/com/drew/imaging/jpeg/package-info.java
new file mode 100644
index 0000000..5a1911f
--- /dev/null
+++ b/Source/com/drew/imaging/jpeg/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with JPEG files.
+ */
+package com.drew.imaging.jpeg;
diff --git a/Source/com/drew/imaging/jpeg/package.html b/Source/com/drew/imaging/jpeg/package.html
deleted file mode 100644
index d65ff55..0000000
--- a/Source/com/drew/imaging/jpeg/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/imaging/package-info.java b/Source/com/drew/imaging/package-info.java
new file mode 100644
index 0000000..5899a9a
--- /dev/null
+++ b/Source/com/drew/imaging/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Contains classes for working with image file formats and photographic conversions.
+ * <!-- Put @see and @since tags down here. -->
+ */
+package com.drew.imaging;
diff --git a/Source/com/drew/imaging/package.html b/Source/com/drew/imaging/package.html
deleted file mode 100644
index af33269..0000000
--- a/Source/com/drew/imaging/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/imaging/pcx/PcxMetadataReader.java b/Source/com/drew/imaging/pcx/PcxMetadataReader.java
new file mode 100644
index 0000000..b1e297a
--- /dev/null
+++ b/Source/com/drew/imaging/pcx/PcxMetadataReader.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.pcx;
+
+import com.drew.lang.StreamReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.file.FileMetadataReader;
+import com.drew.metadata.pcx.PcxReader;
+
+import java.io.*;
+
+/**
+ * Obtains metadata from PCX image files.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PcxMetadataReader
+{
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file) throws IOException
+    {
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
+        try {
+            metadata = readMetadata(inputStream);
+        } finally {
+            inputStream.close();
+        }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream)
+    {
+        Metadata metadata = new Metadata();
+        new PcxReader().extract(new StreamReader(inputStream), metadata);
+        return metadata;
+    }
+}
diff --git a/Source/com/drew/imaging/pcx/package-info.java b/Source/com/drew/imaging/pcx/package-info.java
new file mode 100644
index 0000000..39d214e
--- /dev/null
+++ b/Source/com/drew/imaging/pcx/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with PCX image files.
+ */
+package com.drew.imaging.pcx;
diff --git a/Source/com/drew/imaging/png/PngChromaticities.java b/Source/com/drew/imaging/png/PngChromaticities.java
index 07228ef..094a89e 100644
--- a/Source/com/drew/imaging/png/PngChromaticities.java
+++ b/Source/com/drew/imaging/png/PngChromaticities.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.SequentialByteArrayReader;
diff --git a/Source/com/drew/imaging/png/PngChunk.java b/Source/com/drew/imaging/png/PngChunk.java
index 9a6750f..cbc4bb3 100644
--- a/Source/com/drew/imaging/png/PngChunk.java
+++ b/Source/com/drew/imaging/png/PngChunk.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.annotations.NotNull;
diff --git a/Source/com/drew/imaging/png/PngChunkReader.java b/Source/com/drew/imaging/png/PngChunkReader.java
index e82b7a3..b794514 100644
--- a/Source/com/drew/imaging/png/PngChunkReader.java
+++ b/Source/com/drew/imaging/png/PngChunkReader.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.SequentialReader;
diff --git a/Source/com/drew/imaging/png/PngChunkType.java b/Source/com/drew/imaging/png/PngChunkType.java
index 4bec72f..0a97219 100644
--- a/Source/com/drew/imaging/png/PngChunkType.java
+++ b/Source/com/drew/imaging/png/PngChunkType.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.annotations.NotNull;
@@ -33,7 +53,7 @@ public class PngChunkType
      *     <li><b>interlace method</b> 1 byte, indicates the transmission order of image data, currently only 0 (no interlace) and 1 (Adam7 interlace) are in the standard</li>
      * </ul>
      */
-    public static final PngChunkType IHDR = new PngChunkType("IHDR");
+    public static final PngChunkType IHDR;
 
     /**
      * Denotes a critical {@link PngChunk} that contains palette entries.
@@ -48,25 +68,25 @@ public class PngChunkType
      * </ul>
      * The number of entries is determined by the chunk length. A chunk length indivisible by three is an error.
      */
-    public static final PngChunkType PLTE = new PngChunkType("PLTE");
-    public static final PngChunkType IDAT = new PngChunkType("IDAT", true);
-    public static final PngChunkType IEND = new PngChunkType("IEND");
+    public static final PngChunkType PLTE;
+    public static final PngChunkType IDAT;
+    public static final PngChunkType IEND;
 
     //
     // Standard ancillary chunks
     //
-    public static final PngChunkType cHRM = new PngChunkType("cHRM");
-    public static final PngChunkType gAMA = new PngChunkType("gAMA");
-    public static final PngChunkType iCCP = new PngChunkType("iCCP");
-    public static final PngChunkType sBIT = new PngChunkType("sBIT");
-    public static final PngChunkType sRGB = new PngChunkType("sRGB");
-    public static final PngChunkType bKGD = new PngChunkType("bKGD");
-    public static final PngChunkType hIST = new PngChunkType("hIST");
-    public static final PngChunkType tRNS = new PngChunkType("tRNS");
-    public static final PngChunkType pHYs = new PngChunkType("pHYs");
-    public static final PngChunkType sPLT = new PngChunkType("sPLT", true);
-    public static final PngChunkType tIME = new PngChunkType("tIME");
-    public static final PngChunkType iTXt = new PngChunkType("iTXt", true);
+    public static final PngChunkType cHRM;
+    public static final PngChunkType gAMA;
+    public static final PngChunkType iCCP;
+    public static final PngChunkType sBIT;
+    public static final PngChunkType sRGB;
+    public static final PngChunkType bKGD;
+    public static final PngChunkType hIST;
+    public static final PngChunkType tRNS;
+    public static final PngChunkType pHYs;
+    public static final PngChunkType sPLT;
+    public static final PngChunkType tIME;
+    public static final PngChunkType iTXt;
 
     /**
      * Denotes an ancillary {@link PngChunk} that contains textual data, having first a keyword and then a value.
@@ -81,18 +101,43 @@ public class PngChunkType
      * Text is interpreted according to the Latin-1 character set [ISO-8859-1].
      * Newlines should be represented by a single linefeed character (0x9).
      */
-    public static final PngChunkType tEXt = new PngChunkType("tEXt", true);
-    public static final PngChunkType zTXt = new PngChunkType("zTXt", true);
+    public static final PngChunkType tEXt;
+    public static final PngChunkType zTXt;
+
+    static {
+        try {
+            IHDR = new PngChunkType("IHDR");
+            PLTE = new PngChunkType("PLTE");
+            IDAT = new PngChunkType("IDAT", true);
+            IEND = new PngChunkType("IEND");
+            cHRM = new PngChunkType("cHRM");
+            gAMA = new PngChunkType("gAMA");
+            iCCP = new PngChunkType("iCCP");
+            sBIT = new PngChunkType("sBIT");
+            sRGB = new PngChunkType("sRGB");
+            bKGD = new PngChunkType("bKGD");
+            hIST = new PngChunkType("hIST");
+            tRNS = new PngChunkType("tRNS");
+            pHYs = new PngChunkType("pHYs");
+            sPLT = new PngChunkType("sPLT", true);
+            tIME = new PngChunkType("tIME");
+            iTXt = new PngChunkType("iTXt", true);
+            tEXt = new PngChunkType("tEXt", true);
+            zTXt = new PngChunkType("zTXt", true);
+        } catch (PngProcessingException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
 
     private final byte[] _bytes;
     private final boolean _multipleAllowed;
 
-    public PngChunkType(@NotNull String identifier)
+    public PngChunkType(@NotNull String identifier) throws PngProcessingException
     {
         this(identifier, false);
     }
 
-    public PngChunkType(@NotNull String identifier, boolean multipleAllowed)
+    public PngChunkType(@NotNull String identifier, boolean multipleAllowed) throws PngProcessingException
     {
         _multipleAllowed = multipleAllowed;
 
@@ -105,22 +150,22 @@ public class PngChunkType
         }
     }
 
-    public PngChunkType(@NotNull byte[] bytes)
+    public PngChunkType(@NotNull byte[] bytes) throws PngProcessingException
     {
         validateBytes(bytes);
         _bytes = bytes;
         _multipleAllowed = _identifiersAllowingMultiples.contains(getIdentifier());
     }
 
-    private static void validateBytes(byte[] bytes)
+    private static void validateBytes(byte[] bytes) throws PngProcessingException
     {
         if (bytes.length != 4) {
-            throw new IllegalArgumentException("PNG chunk type identifier must be four bytes in length");
+            throw new PngProcessingException("PNG chunk type identifier must be four bytes in length");
         }
 
         for (byte b : bytes) {
             if (!isValidByte(b)) {
-                throw new IllegalArgumentException("PNG chunk type identifier may only contain alphabet characters");
+                throw new PngProcessingException("PNG chunk type identifier may only contain alphabet characters");
             }
         }
     }
diff --git a/Source/com/drew/imaging/png/PngColorType.java b/Source/com/drew/imaging/png/PngColorType.java
index 5927ee9..6d02bed 100644
--- a/Source/com/drew/imaging/png/PngColorType.java
+++ b/Source/com/drew/imaging/png/PngColorType.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.annotations.NotNull;
diff --git a/Source/com/drew/imaging/png/PngHeader.java b/Source/com/drew/imaging/png/PngHeader.java
index 12ed220..81fef4f 100644
--- a/Source/com/drew/imaging/png/PngHeader.java
+++ b/Source/com/drew/imaging/png/PngHeader.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.SequentialByteArrayReader;
diff --git a/Source/com/drew/imaging/png/PngMetadataReader.java b/Source/com/drew/imaging/png/PngMetadataReader.java
index 8cfe31b..4068818 100644
--- a/Source/com/drew/imaging/png/PngMetadataReader.java
+++ b/Source/com/drew/imaging/png/PngMetadataReader.java
@@ -1,14 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.*;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
+import com.drew.metadata.file.FileMetadataReader;
 import com.drew.metadata.icc.IccReader;
 import com.drew.metadata.png.PngChromaticitiesDirectory;
 import com.drew.metadata.png.PngDirectory;
 import com.drew.metadata.xmp.XmpReader;
 
 import java.io.*;
+import java.nio.charset.Charset;
 import java.util.*;
 import java.util.zip.InflaterInputStream;
 
@@ -17,24 +40,25 @@ import java.util.zip.InflaterInputStream;
  */
 public class PngMetadataReader
 {
-    @NotNull
-    public static Metadata readMetadata(@NotNull File file) throws PngProcessingException, IOException
-    {
-        InputStream inputStream = null;
-        try {
-            inputStream = new FileInputStream(file);
-            return readMetadata(inputStream);
-        } finally {
-            if (inputStream != null)
-                inputStream.close();
-        }
-    }
+    private static Set<PngChunkType> _desiredChunkTypes;
 
-    @NotNull
-    public static Metadata readMetadata(@NotNull InputStream inputStream) throws PngProcessingException, IOException
+    /**
+     * The PNG spec states that ISO_8859_1 (Latin-1) encoding should be used for:
+     * <ul>
+     *   <li>"tEXt" and "zTXt" chunks, both for keys and values (https://www.w3.org/TR/PNG/#11tEXt)</li>
+     *   <li>"iCCP" chunks, for the profile name (https://www.w3.org/TR/PNG/#11iCCP)</li>
+     *   <li>"sPLT" chunks, for the palette name (https://www.w3.org/TR/PNG/#11sPLT)</li>
+     * </ul>
+     * Note that "iTXt" chunks use UTF-8 encoding (https://www.w3.org/TR/PNG/#11iTXt).
+     * <p/>
+     * For more guidance: http://www.w3.org/TR/PNG-Decoders.html#D.Text-chunk-processing
+     */
+    private static Charset _latin1Encoding = Charsets.ISO_8859_1;
+
+    static
     {
-        // TODO keep a single static hash of these
         Set<PngChunkType> desiredChunkTypes = new HashSet<PngChunkType>();
+
         desiredChunkTypes.add(PngChunkType.IHDR);
         desiredChunkTypes.add(PngChunkType.PLTE);
         desiredChunkTypes.add(PngChunkType.tRNS);
@@ -44,127 +68,262 @@ public class PngMetadataReader
         desiredChunkTypes.add(PngChunkType.iCCP);
         desiredChunkTypes.add(PngChunkType.bKGD);
         desiredChunkTypes.add(PngChunkType.tEXt);
+        desiredChunkTypes.add(PngChunkType.zTXt);
         desiredChunkTypes.add(PngChunkType.iTXt);
         desiredChunkTypes.add(PngChunkType.tIME);
+        desiredChunkTypes.add(PngChunkType.pHYs);
+        desiredChunkTypes.add(PngChunkType.sBIT);
 
-        Iterable<PngChunk> chunks = new PngChunkReader().extract(new StreamReader(inputStream), desiredChunkTypes);
+        _desiredChunkTypes = Collections.unmodifiableSet(desiredChunkTypes);
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file) throws PngProcessingException, IOException
+    {
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
+        try {
+            metadata = readMetadata(inputStream);
+        } finally {
+            inputStream.close();
+        }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream) throws PngProcessingException, IOException
+    {
+        Iterable<PngChunk> chunks = new PngChunkReader().extract(new StreamReader(inputStream), _desiredChunkTypes);
 
         Metadata metadata = new Metadata();
-        List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>();
 
         for (PngChunk chunk : chunks) {
-            PngChunkType chunkType = chunk.getType();
-            byte[] bytes = chunk.getBytes();
-
-            if (chunkType.equals(PngChunkType.IHDR)) {
-                PngHeader header = new PngHeader(bytes);
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setInt(PngDirectory.TAG_IMAGE_WIDTH, header.getImageWidth());
-                directory.setInt(PngDirectory.TAG_IMAGE_HEIGHT, header.getImageHeight());
-                directory.setInt(PngDirectory.TAG_BITS_PER_SAMPLE, header.getBitsPerSample());
-                directory.setInt(PngDirectory.TAG_COLOR_TYPE, header.getColorType().getNumericValue());
-                directory.setInt(PngDirectory.TAG_COMPRESSION_TYPE, header.getCompressionType());
-                directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.getFilterMethod());
-                directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.getInterlaceMethod());
-            } else if (chunkType.equals(PngChunkType.PLTE)) {
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.length / 3);
-            } else if (chunkType.equals(PngChunkType.tRNS)) {
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setInt(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, 1);
-            } else if (chunkType.equals(PngChunkType.sRGB)) {
-                int srgbRenderingIntent = new SequentialByteArrayReader(bytes).getInt8();
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setInt(PngDirectory.TAG_SRGB_RENDERING_INTENT, srgbRenderingIntent);
-            } else if (chunkType.equals(PngChunkType.cHRM)) {
-                PngChromaticities chromaticities = new PngChromaticities(bytes);
-                PngChromaticitiesDirectory directory = metadata.getOrCreateDirectory(PngChromaticitiesDirectory.class);
-                directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.getWhitePointX());
-                directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.getWhitePointX());
-                directory.setInt(PngChromaticitiesDirectory.TAG_RED_X, chromaticities.getRedX());
-                directory.setInt(PngChromaticitiesDirectory.TAG_RED_Y, chromaticities.getRedY());
-                directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_X, chromaticities.getGreenX());
-                directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_Y, chromaticities.getGreenY());
-                directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_X, chromaticities.getBlueX());
-                directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_Y, chromaticities.getBlueY());
-            } else if (chunkType.equals(PngChunkType.gAMA)) {
-                int gammaInt = new SequentialByteArrayReader(bytes).getInt32();
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setDouble(PngDirectory.TAG_GAMMA, gammaInt / 100000.0);
-            } else if (chunkType.equals(PngChunkType.iCCP)) {
-                SequentialReader reader = new SequentialByteArrayReader(bytes);
-                String profileName = reader.getNullTerminatedString(79);
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setString(PngDirectory.TAG_ICC_PROFILE_NAME, profileName);
-                byte compressionMethod = reader.getInt8();
-                if (compressionMethod == 0) {
-                    // Only compression method allowed by the spec is zero: deflate
-                    // This assumes 1-byte-per-char, which it is by spec.
-                    int bytesLeft = bytes.length - profileName.length() - 2;
-                    byte[] compressedProfile = reader.getBytes(bytesLeft);
+            try {
+                processChunk(metadata, chunk);
+            } catch (Exception e) {
+                e.printStackTrace(System.err);
+            }
+        }
+
+        return metadata;
+    }
+
+    private static void processChunk(@NotNull Metadata metadata, @NotNull PngChunk chunk) throws PngProcessingException, IOException
+    {
+        PngChunkType chunkType = chunk.getType();
+        byte[] bytes = chunk.getBytes();
+
+        if (chunkType.equals(PngChunkType.IHDR)) {
+            PngHeader header = new PngHeader(bytes);
+            PngDirectory directory = new PngDirectory(PngChunkType.IHDR);
+            directory.setInt(PngDirectory.TAG_IMAGE_WIDTH, header.getImageWidth());
+            directory.setInt(PngDirectory.TAG_IMAGE_HEIGHT, header.getImageHeight());
+            directory.setInt(PngDirectory.TAG_BITS_PER_SAMPLE, header.getBitsPerSample());
+            directory.setInt(PngDirectory.TAG_COLOR_TYPE, header.getColorType().getNumericValue());
+            directory.setInt(PngDirectory.TAG_COMPRESSION_TYPE, header.getCompressionType() & 0xFF); // make sure it's unsigned
+            directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.getFilterMethod());
+            directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.getInterlaceMethod());
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.PLTE)) {
+            PngDirectory directory = new PngDirectory(PngChunkType.PLTE);
+            directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.length / 3);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.tRNS)) {
+            PngDirectory directory = new PngDirectory(PngChunkType.tRNS);
+            directory.setInt(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, 1);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.sRGB)) {
+            int srgbRenderingIntent = bytes[0];
+            PngDirectory directory = new PngDirectory(PngChunkType.sRGB);
+            directory.setInt(PngDirectory.TAG_SRGB_RENDERING_INTENT, srgbRenderingIntent);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.cHRM)) {
+            PngChromaticities chromaticities = new PngChromaticities(bytes);
+            PngChromaticitiesDirectory directory = new PngChromaticitiesDirectory();
+            directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.getWhitePointX());
+            directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_Y, chromaticities.getWhitePointY());
+            directory.setInt(PngChromaticitiesDirectory.TAG_RED_X, chromaticities.getRedX());
+            directory.setInt(PngChromaticitiesDirectory.TAG_RED_Y, chromaticities.getRedY());
+            directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_X, chromaticities.getGreenX());
+            directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_Y, chromaticities.getGreenY());
+            directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_X, chromaticities.getBlueX());
+            directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_Y, chromaticities.getBlueY());
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.gAMA)) {
+            int gammaInt = ByteConvert.toInt32BigEndian(bytes);
+            new SequentialByteArrayReader(bytes).getInt32();
+            PngDirectory directory = new PngDirectory(PngChunkType.gAMA);
+            directory.setDouble(PngDirectory.TAG_GAMMA, gammaInt / 100000.0);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.iCCP)) {
+            SequentialReader reader = new SequentialByteArrayReader(bytes);
+
+            // Profile Name is 1-79 bytes, followed by the 1 byte null character
+            byte[] profileNameBytes = reader.getNullTerminatedBytes(79 + 1);
+            PngDirectory directory = new PngDirectory(PngChunkType.iCCP);
+            directory.setStringValue(PngDirectory.TAG_ICC_PROFILE_NAME, new StringValue(profileNameBytes, _latin1Encoding));
+            byte compressionMethod = reader.getInt8();
+            // Only compression method allowed by the spec is zero: deflate
+            if (compressionMethod == 0) {
+                // bytes left for compressed text is:
+                // total bytes length - (profilenamebytes length + null byte + compression method byte)
+                int bytesLeft = bytes.length - (profileNameBytes.length + 1 + 1);
+                byte[] compressedProfile = reader.getBytes(bytesLeft);
+
+                try {
                     InflaterInputStream inflateStream = new InflaterInputStream(new ByteArrayInputStream(compressedProfile));
-                    new IccReader().extract(new RandomAccessStreamReader(inflateStream), metadata);
+                    new IccReader().extract(new RandomAccessStreamReader(inflateStream), metadata, directory);
                     inflateStream.close();
+                } catch(java.util.zip.ZipException zex) {
+                    directory.addError(String.format("Exception decompressing PNG iCCP chunk : %s", zex.getMessage()));
+                    metadata.addDirectory(directory);
                 }
-            } else if (chunkType.equals(PngChunkType.bKGD)) {
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setByteArray(PngDirectory.TAG_BACKGROUND_COLOR, bytes);
-            } else if (chunkType.equals(PngChunkType.tEXt)) {
-                SequentialReader reader = new SequentialByteArrayReader(bytes);
-                String keyword = reader.getNullTerminatedString(79);
-                int bytesLeft = bytes.length - keyword.length() - 1;
-                String value = reader.getNullTerminatedString(bytesLeft);
-                textPairs.add(new KeyValuePair(keyword, value));
-            } else if (chunkType.equals(PngChunkType.iTXt)) {
-                SequentialReader reader = new SequentialByteArrayReader(bytes);
-                String keyword = reader.getNullTerminatedString(79);
-                byte compressionFlag = reader.getInt8();
-                byte compressionMethod = reader.getInt8();
-                String languageTag = reader.getNullTerminatedString(bytes.length);
-                String translatedKeyword = reader.getNullTerminatedString(bytes.length);
-                int bytesLeft = bytes.length - keyword.length() - 1 - 1 - 1 - languageTag.length() - 1 - translatedKeyword.length() - 1;
-                String text = null;
-                if (compressionFlag == 0) {
-                    text = reader.getNullTerminatedString(bytesLeft);
-                } else if (compressionFlag == 1) {
-                    if (compressionMethod == 0) {
-                        text = StringUtil.fromStream(new InflaterInputStream(new ByteArrayInputStream(bytes, bytes.length - bytesLeft, bytesLeft)));
-                    } else {
-                        metadata.getOrCreateDirectory(PngDirectory.class).addError("Invalid compression method value");
-                    }
+            } else {
+                directory.addError("Invalid compression method value");
+            }
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.bKGD)) {
+            PngDirectory directory = new PngDirectory(PngChunkType.bKGD);
+            directory.setByteArray(PngDirectory.TAG_BACKGROUND_COLOR, bytes);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.tEXt)) {
+            SequentialReader reader = new SequentialByteArrayReader(bytes);
+
+            // Keyword is 1-79 bytes, followed by the 1 byte null character
+            StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding);
+            String keyword = keywordsv.toString();
+
+            // bytes left for text is:
+            // total bytes length - (Keyword length + null byte)
+            int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1);
+            StringValue value = reader.getNullTerminatedStringValue(bytesLeft, _latin1Encoding);
+            List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>();
+            textPairs.add(new KeyValuePair(keyword, value));
+            PngDirectory directory = new PngDirectory(PngChunkType.tEXt);
+            directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.zTXt)) {
+            SequentialReader reader = new SequentialByteArrayReader(bytes);
+
+            // Keyword is 1-79 bytes, followed by the 1 byte null character
+            StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding);
+            String keyword = keywordsv.toString();
+            byte compressionMethod = reader.getInt8();
+
+            // bytes left for compressed text is:
+            // total bytes length - (Keyword length + null byte + compression method byte)
+            int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1 + 1);
+            byte[] textBytes = null;
+            if (compressionMethod == 0) {
+                try {
+                    textBytes = StreamUtil.readAllBytes(new InflaterInputStream(new ByteArrayInputStream(bytes, bytes.length - bytesLeft, bytesLeft)));
+                } catch(java.util.zip.ZipException zex) {
+                    textBytes = null;
+                    PngDirectory directory = new PngDirectory(PngChunkType.zTXt);
+                    directory.addError(String.format("Exception decompressing PNG zTXt chunk with keyword \"%s\": %s", keyword, zex.getMessage()));
+                    metadata.addDirectory(directory);
+                }
+            } else {
+                PngDirectory directory = new PngDirectory(PngChunkType.zTXt);
+                directory.addError("Invalid compression method value");
+                metadata.addDirectory(directory);
+            }
+            if (textBytes != null) {
+                if (keyword.equals("XML:com.adobe.xmp")) {
+                    // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
+                    new XmpReader().extract(textBytes, metadata);
                 } else {
-                    metadata.getOrCreateDirectory(PngDirectory.class).addError("Invalid compression flag value");
+                    List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>();
+                    textPairs.add(new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding)));
+                    PngDirectory directory = new PngDirectory(PngChunkType.zTXt);
+                    directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs);
+                    metadata.addDirectory(directory);
                 }
+            }
+        } else if (chunkType.equals(PngChunkType.iTXt)) {
+            SequentialReader reader = new SequentialByteArrayReader(bytes);
+
+            // Keyword is 1-79 bytes, followed by the 1 byte null character
+            StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding);
+            String keyword = keywordsv.toString();
+            byte compressionFlag = reader.getInt8();
+            byte compressionMethod = reader.getInt8();
+            // TODO we currently ignore languageTagBytes and translatedKeywordBytes
+            byte[] languageTagBytes = reader.getNullTerminatedBytes(bytes.length);
+            byte[] translatedKeywordBytes = reader.getNullTerminatedBytes(bytes.length);
 
-                if (text != null) {
-                    if (keyword.equals("XML:com.adobe.xmp")) {
-                        // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
-                        new XmpReader().extract(text, metadata);
-                    } else {
-                        textPairs.add(new KeyValuePair(keyword, text));
+            // bytes left for compressed text is:
+            // total bytes length - (Keyword length + null byte + comp flag byte + comp method byte + lang length + null byte + translated length + null byte)
+            int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1 + 1 + 1 + languageTagBytes.length + 1 + translatedKeywordBytes.length + 1);
+            byte[] textBytes = null;
+            if (compressionFlag == 0) {
+                textBytes = reader.getNullTerminatedBytes(bytesLeft);
+            } else if (compressionFlag == 1) {
+                if (compressionMethod == 0) {
+                    try {
+                        textBytes = StreamUtil.readAllBytes(new InflaterInputStream(new ByteArrayInputStream(bytes, bytes.length - bytesLeft, bytesLeft)));
+                    } catch(java.util.zip.ZipException zex) {
+                        textBytes = null;
+                        PngDirectory directory = new PngDirectory(PngChunkType.iTXt);
+                        directory.addError(String.format("Exception decompressing PNG iTXt chunk with keyword \"%s\": %s", keyword, zex.getMessage()));
+                        metadata.addDirectory(directory);
                     }
+                } else {
+                    PngDirectory directory = new PngDirectory(PngChunkType.iTXt);
+                    directory.addError("Invalid compression method value");
+                    metadata.addDirectory(directory);
                 }
-            } else if (chunkType.equals(PngChunkType.tIME)) {
-                SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes);
-                int year = reader.getUInt16();
-                int month = reader.getUInt8() - 1;
-                int day = reader.getUInt8();
-                int hour = reader.getUInt8();
-                int minute = reader.getUInt8();
-                int second = reader.getUInt8();
-                Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-                //noinspection MagicConstant
-                calendar.set(year, month, day, hour, minute, second);
-                PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-                directory.setDate(PngDirectory.TAG_LAST_MODIFICATION_TIME, calendar.getTime());
+            } else {
+                PngDirectory directory = new PngDirectory(PngChunkType.iTXt);
+                directory.addError("Invalid compression flag value");
+                metadata.addDirectory(directory);
             }
-        }
 
-        if (textPairs.size() != 0) {
-            PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class);
-            directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs);
+            if (textBytes != null) {
+                if (keyword.equals("XML:com.adobe.xmp")) {
+                    // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
+                    new XmpReader().extract(textBytes, metadata);
+                } else {
+                    List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>();
+                    textPairs.add(new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding)));
+                    PngDirectory directory = new PngDirectory(PngChunkType.iTXt);
+                    directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs);
+                    metadata.addDirectory(directory);
+                }
+            }
+        } else if (chunkType.equals(PngChunkType.tIME)) {
+            SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes);
+            int year = reader.getUInt16();
+            int month = reader.getUInt8();
+            int day = reader.getUInt8();
+            int hour = reader.getUInt8();
+            int minute = reader.getUInt8();
+            int second = reader.getUInt8();
+            PngDirectory directory = new PngDirectory(PngChunkType.tIME);
+            if (DateUtil.isValidDate(year, month - 1, day) && DateUtil.isValidTime(hour, minute, second)) {
+                String dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
+                directory.setString(PngDirectory.TAG_LAST_MODIFICATION_TIME, dateString);
+            } else {
+                directory.addError(String.format(
+                    "PNG tIME data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d",
+                    year, month, day, hour, minute, second));
+            }
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.pHYs)) {
+            SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes);
+            int pixelsPerUnitX = reader.getInt32();
+            int pixelsPerUnitY = reader.getInt32();
+            byte unitSpecifier = reader.getInt8();
+            PngDirectory directory = new PngDirectory(PngChunkType.pHYs);
+            directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_X, pixelsPerUnitX);
+            directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_Y, pixelsPerUnitY);
+            directory.setInt(PngDirectory.TAG_UNIT_SPECIFIER, unitSpecifier);
+            metadata.addDirectory(directory);
+        } else if (chunkType.equals(PngChunkType.sBIT)) {
+            PngDirectory directory = new PngDirectory(PngChunkType.sBIT);
+            directory.setByteArray(PngDirectory.TAG_SIGNIFICANT_BITS, bytes);
+            metadata.addDirectory(directory);
         }
-
-        return metadata;
     }
 }
diff --git a/Source/com/drew/imaging/png/PngProcessingException.java b/Source/com/drew/imaging/png/PngProcessingException.java
index 19baab4..3095069 100644
--- a/Source/com/drew/imaging/png/PngProcessingException.java
+++ b/Source/com/drew/imaging/png/PngProcessingException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/png/package-info.java b/Source/com/drew/imaging/png/package-info.java
new file mode 100644
index 0000000..eb1e3d4
--- /dev/null
+++ b/Source/com/drew/imaging/png/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for working with PNG (Portable Network Graphic) files.
+ *
+ * @since 2.7.0
+ */
+package com.drew.imaging.png;
diff --git a/Source/com/drew/imaging/png/package.html b/Source/com/drew/imaging/png/package.html
deleted file mode 100644
index d0885b3..0000000
--- a/Source/com/drew/imaging/png/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with PNG (Portable Network Graphic) files.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
diff --git a/Source/com/drew/imaging/psd/PsdMetadataReader.java b/Source/com/drew/imaging/psd/PsdMetadataReader.java
index f46e9b6..44feffe 100644
--- a/Source/com/drew/imaging/psd/PsdMetadataReader.java
+++ b/Source/com/drew/imaging/psd/PsdMetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,10 +21,10 @@
 
 package com.drew.imaging.psd;
 
-import com.drew.lang.RandomAccessFileReader;
-import com.drew.lang.RandomAccessStreamReader;
+import com.drew.lang.StreamReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.file.FileMetadataReader;
 import com.drew.metadata.photoshop.PsdReader;
 
 import java.io.*;
@@ -40,15 +40,13 @@ public class PsdMetadataReader
     public static Metadata readMetadata(@NotNull File file) throws IOException
     {
         Metadata metadata = new Metadata();
-
-        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
-
+        InputStream stream = new FileInputStream(file);
         try {
-            new PsdReader().extract(new RandomAccessFileReader(randomAccessFile), metadata);
+            new PsdReader().extract(new StreamReader(stream), metadata);
         } finally {
-            randomAccessFile.close();
+            stream.close();
         }
-
+        new FileMetadataReader().read(file, metadata);
         return metadata;
     }
 
@@ -56,7 +54,7 @@ public class PsdMetadataReader
     public static Metadata readMetadata(@NotNull InputStream inputStream)
     {
         Metadata metadata = new Metadata();
-        new PsdReader().extract(new RandomAccessStreamReader(inputStream), metadata);
+        new PsdReader().extract(new StreamReader(inputStream), metadata);
         return metadata;
     }
 }
diff --git a/Source/com/drew/imaging/psd/package-info.java b/Source/com/drew/imaging/psd/package-info.java
new file mode 100644
index 0000000..77a0afe
--- /dev/null
+++ b/Source/com/drew/imaging/psd/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with PSD (PhotoShop Document) files.
+ */
+package com.drew.imaging.psd;
diff --git a/Source/com/drew/imaging/psd/package.html b/Source/com/drew/imaging/psd/package.html
deleted file mode 100644
index 315f536..0000000
--- a/Source/com/drew/imaging/psd/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for working with PSD (PhotoShop Document) files.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/imaging/raf/RafMetadataReader.java b/Source/com/drew/imaging/raf/RafMetadataReader.java
new file mode 100644
index 0000000..2a3b5ae
--- /dev/null
+++ b/Source/com/drew/imaging/raf/RafMetadataReader.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.raf;
+
+import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.imaging.jpeg.JpegProcessingException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Obtains metadata from RAF (Fujifilm camera raw) image files.
+ *
+ * @author TSGames https://github.com/TSGames
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RafMetadataReader
+{
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException, IOException
+    {
+        if (!inputStream.markSupported())
+            throw new IOException("Stream must support mark/reset");
+
+        inputStream.mark(512);
+
+        byte[] data = new byte[512];
+        int bytesRead = inputStream.read(data);
+
+        if (bytesRead == -1)
+            throw new IOException("Stream is empty");
+
+        inputStream.reset();
+
+        for (int i = 0; i < bytesRead - 2; i++) {
+            // Look for the first three bytes of a JPEG encoded file
+            if (data[i] == (byte) 0xff && data[i + 1] == (byte) 0xd8 && data[i + 2] == (byte) 0xff) {
+                long bytesSkipped = inputStream.skip(i);
+                if (bytesSkipped != i)
+                    throw new IOException("Skipping stream bytes failed");
+                break;
+            }
+        }
+
+        return JpegMetadataReader.readMetadata(inputStream);
+    }
+
+    private RafMetadataReader() throws Exception
+    {
+        throw new Exception("Not intended for instantiation");
+    }
+}
diff --git a/Source/com/drew/imaging/raf/package-info.java b/Source/com/drew/imaging/raf/package-info.java
new file mode 100644
index 0000000..17e130f
--- /dev/null
+++ b/Source/com/drew/imaging/raf/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with RAF (Fujifilm camera raw) format files.
+ */
+package com.drew.imaging.raf;
diff --git a/Source/com/drew/imaging/riff/RiffHandler.java b/Source/com/drew/imaging/riff/RiffHandler.java
new file mode 100644
index 0000000..f6b9819
--- /dev/null
+++ b/Source/com/drew/imaging/riff/RiffHandler.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.riff;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Interface of an class capable of handling events raised during the reading of a RIFF file
+ * via {@link RiffReader}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public interface RiffHandler
+{
+    /**
+     * Gets whether the specified RIFF identifier is of interest to this handler.
+     * Returning <code>false</code> causes processing to stop after reading only
+     * the first twelve bytes of data.
+     *
+     * @param identifier The four character code identifying the type of RIFF data
+     * @return true if processing should continue, otherwise false
+     */
+    boolean shouldAcceptRiffIdentifier(@NotNull String identifier);
+
+    /**
+     * Gets whether this handler is interested in the specific chunk type.
+     * Returns <code>true</code> if the data should be copied into an array and passed
+     * to {@link RiffHandler#processChunk(String, byte[])}, or <code>false</code> to avoid
+     * the copy and skip to the next chunk in the file, if any.
+     *
+     * @param fourCC the four character code of this chunk
+     * @return true if {@link RiffHandler#processChunk(String, byte[])} should be called, otherwise false
+     */
+    boolean shouldAcceptChunk(@NotNull String fourCC);
+
+    /**
+     * Perform whatever processing is necessary for the type of chunk with its
+     * payload.
+     *
+     * This is only called if a previous call to {@link RiffHandler#shouldAcceptChunk(String)}
+     * with the same <code>fourCC</code> returned <code>true</code>.
+     *
+     * @param fourCC the four character code of the chunk
+     * @param payload they payload of the chunk as a byte array
+     */
+    void processChunk(@NotNull String fourCC, @NotNull byte[] payload);
+}
diff --git a/Source/com/drew/imaging/riff/RiffProcessingException.java b/Source/com/drew/imaging/riff/RiffProcessingException.java
new file mode 100644
index 0000000..8ffc035
--- /dev/null
+++ b/Source/com/drew/imaging/riff/RiffProcessingException.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.imaging.riff;
+
+import com.drew.imaging.ImageProcessingException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An exception class thrown upon unexpected and fatal conditions while processing a RIFF file.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RiffProcessingException extends ImageProcessingException
+{
+    private static final long serialVersionUID = -1658134596321487960L;
+
+    public RiffProcessingException(@Nullable String message)
+    {
+        super(message);
+    }
+
+    public RiffProcessingException(@Nullable String message, @Nullable Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public RiffProcessingException(@Nullable Throwable cause)
+    {
+        super(cause);
+    }
+}
diff --git a/Source/com/drew/imaging/riff/RiffReader.java b/Source/com/drew/imaging/riff/RiffReader.java
new file mode 100644
index 0000000..6decdec
--- /dev/null
+++ b/Source/com/drew/imaging/riff/RiffReader.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.riff;
+
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+
+/**
+ * Processes RIFF-formatted data, calling into client code via that {@link RiffHandler} interface.
+ * <p></p>
+ * For information on this file format, see:
+ * <ul>
+ *     <li>http://en.wikipedia.org/wiki/Resource_Interchange_File_Format</li>
+ *     <li>https://developers.google.com/speed/webp/docs/riff_container</li>
+ *     <li>https://www.daubnet.com/en/file-format-riff</li>
+ * </ul>
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class RiffReader
+{
+    /**
+     * Processes a RIFF data sequence.
+     *
+     * @param reader the {@link SequentialReader} from which the data should be read
+     * @param handler the {@link RiffHandler} that will coordinate processing and accept read values
+     * @throws RiffProcessingException if an error occurred during the processing of RIFF data that could not be
+     *                                 ignored or recovered from
+     * @throws IOException an error occurred while accessing the required data
+     */
+    public void processRiff(@NotNull final SequentialReader reader,
+                            @NotNull final RiffHandler handler) throws RiffProcessingException, IOException
+    {
+        // RIFF files are always little-endian
+        reader.setMotorolaByteOrder(false);
+
+        // PROCESS FILE HEADER
+
+        final String fileFourCC = reader.getString(4);
+
+        if (!fileFourCC.equals("RIFF"))
+            throw new RiffProcessingException("Invalid RIFF header: " + fileFourCC);
+
+        // The total size of the chunks that follow plus 4 bytes for the 'WEBP' FourCC
+        final int fileSize = reader.getInt32();
+        int sizeLeft = fileSize;
+
+        final String identifier = reader.getString(4);
+        sizeLeft -= 4;
+
+        if (!handler.shouldAcceptRiffIdentifier(identifier))
+            return;
+
+        // PROCESS CHUNKS
+
+        while (sizeLeft != 0) {
+            final String chunkFourCC = reader.getString(4);
+            final int chunkSize = reader.getInt32();
+            sizeLeft -= 8;
+
+            // NOTE we fail a negative chunk size here (greater than 0x7FFFFFFF) as Java cannot
+            // allocate arrays larger than this.
+            if (chunkSize < 0 || sizeLeft < chunkSize)
+                throw new RiffProcessingException("Invalid RIFF chunk size");
+
+            if (handler.shouldAcceptChunk(chunkFourCC)) {
+                // TODO is it feasible to avoid copying the chunk here, and to pass the sequential reader to the handler?
+                handler.processChunk(chunkFourCC, reader.getBytes(chunkSize));
+            } else {
+                reader.skip(chunkSize);
+            }
+
+            sizeLeft -= chunkSize;
+
+            // Skip any padding byte added to keep chunks aligned to even numbers of bytes
+            if (chunkSize % 2 == 1) {
+                reader.getInt8();
+                sizeLeft--;
+            }
+        }
+    }
+}
diff --git a/Source/com/drew/imaging/riff/package-info.java b/Source/com/drew/imaging/riff/package-info.java
new file mode 100644
index 0000000..1fd4a4b
--- /dev/null
+++ b/Source/com/drew/imaging/riff/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with RIFF format files, such as WebP.
+ */
+package com.drew.imaging.riff;
diff --git a/Source/com/drew/imaging/tiff/TiffDataFormat.java b/Source/com/drew/imaging/tiff/TiffDataFormat.java
index 765cd71..bf99dc4 100644
--- a/Source/com/drew/imaging/tiff/TiffDataFormat.java
+++ b/Source/com/drew/imaging/tiff/TiffDataFormat.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/tiff/TiffHandler.java b/Source/com/drew/imaging/tiff/TiffHandler.java
index 94b2512..fd24d06 100644
--- a/Source/com/drew/imaging/tiff/TiffHandler.java
+++ b/Source/com/drew/imaging/tiff/TiffHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -23,11 +23,16 @@ package com.drew.imaging.tiff;
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.Rational;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 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
@@ -42,12 +47,13 @@ public interface TiffHandler
      */
     void setTiffMarker(int marker) throws TiffProcessingException;
 
-    boolean isTagIfdPointer(int tagType);
+    boolean tryEnterSubIfd(int tagId);
     boolean hasFollowerIfd();
 
     void endingIFD();
 
-    void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset);
+    @Nullable
+    Long tryCustomProcessFormat(int tagId, int formatCode, long componentCount);
 
     boolean customProcessTag(int tagOffset,
                              @NotNull Set<Integer> processedIfdOffsets,
@@ -60,7 +66,7 @@ public interface TiffHandler
     void error(@NotNull String message);
 
     void setByteArray(int tagId, @NotNull byte[] bytes);
-    void setString(int tagId, @NotNull String string);
+    void setString(int tagId, @NotNull StringValue string);
     void setRational(int tagId, @NotNull Rational rational);
     void setRationalArray(int tagId, @NotNull Rational[] array);
     void setFloat(int tagId, float float32);
diff --git a/Source/com/drew/imaging/tiff/TiffMetadataReader.java b/Source/com/drew/imaging/tiff/TiffMetadataReader.java
index d56ee5f..aaf4dc5 100644
--- a/Source/com/drew/imaging/tiff/TiffMetadataReader.java
+++ b/Source/com/drew/imaging/tiff/TiffMetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,15 +21,14 @@
 package com.drew.imaging.tiff;
 
 import com.drew.lang.RandomAccessFileReader;
+import com.drew.lang.RandomAccessReader;
 import com.drew.lang.RandomAccessStreamReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.exif.ExifTiffHandler;
+import com.drew.metadata.file.FileMetadataReader;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.RandomAccessFile;
+import java.io.*;
 
 /**
  * Obtains all available metadata from TIFF formatted files.  Note that TIFF files include many digital camera RAW
@@ -47,12 +46,14 @@ public class TiffMetadataReader
         RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
 
         try {
-            ExifTiffHandler handler = new ExifTiffHandler(metadata, false);
+            ExifTiffHandler handler = new ExifTiffHandler(metadata, null);
             new TiffReader().processTiff(new RandomAccessFileReader(randomAccessFile), handler, 0);
         } finally {
             randomAccessFile.close();
         }
 
+        new FileMetadataReader().read(file, metadata);
+
         return metadata;
     }
 
@@ -63,9 +64,15 @@ public class TiffMetadataReader
         // InputStream does not support seeking backwards, so we wrap it with RandomAccessStreamReader, which
         // buffers data from the stream as we seek forward.
 
+        return readMetadata(new RandomAccessStreamReader(inputStream));
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull RandomAccessReader reader) throws IOException, TiffProcessingException
+    {
         Metadata metadata = new Metadata();
-        ExifTiffHandler handler = new ExifTiffHandler(metadata, false);
-        new TiffReader().processTiff(new RandomAccessStreamReader(inputStream), handler, 0);
+        ExifTiffHandler handler = new ExifTiffHandler(metadata, null);
+        new TiffReader().processTiff(reader, handler, 0);
         return metadata;
     }
 }
diff --git a/Source/com/drew/imaging/tiff/TiffProcessingException.java b/Source/com/drew/imaging/tiff/TiffProcessingException.java
index 5d90c9c..123cc21 100644
--- a/Source/com/drew/imaging/tiff/TiffProcessingException.java
+++ b/Source/com/drew/imaging/tiff/TiffProcessingException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/imaging/tiff/TiffReader.java b/Source/com/drew/imaging/tiff/TiffReader.java
index 497a64e..dd4aefe 100644
--- a/Source/com/drew/imaging/tiff/TiffReader.java
+++ b/Source/com/drew/imaging/tiff/TiffReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -76,8 +76,6 @@ public class TiffReader
 
         Set<Integer> processedIfdOffsets = new HashSet<Integer>();
         processIfd(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset);
-
-        handler.completed(reader, tiffHeaderOffset);
     }
 
     /**
@@ -109,6 +107,7 @@ public class TiffReader
                                   final int ifdOffset,
                                   final int tiffHeaderOffset) throws IOException
     {
+        Boolean resetByteOrder = null;
         try {
             // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
             if (processedIfdOffsets.contains(Integer.valueOf(ifdOffset))) {
@@ -126,6 +125,16 @@ public class TiffReader
             // First two bytes in the IFD are the number of tags in this directory
             int dirTagCount = reader.getUInt16(ifdOffset);
 
+            // Some software modifies the byte order of the file, but misses some IFDs (such as makernotes).
+            // The entire test image repository doesn't contain a single IFD with more than 255 entries.
+            // Here we detect switched bytes that suggest this problem, and temporarily swap the byte order.
+            // This was discussed in GitHub issue #136.
+            if (dirTagCount > 0xFF && (dirTagCount & 0xFF) == 0) {
+                resetByteOrder = reader.isMotorolaByteOrder();
+                dirTagCount >>= 8;
+                reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder());
+            }
+
             int dirLength = (2 + (12 * dirTagCount) + 4);
             if (dirLength + ifdOffset > reader.getLength()) {
                 handler.error("Illegally sized IFD");
@@ -146,31 +155,32 @@ public class TiffReader
                 final int formatCode = reader.getUInt16(tagOffset + 2);
                 final TiffDataFormat format = TiffDataFormat.fromTiffFormatCode(formatCode);
 
+                // 4 bytes dictate the number of components in this tag's data
+                final long componentCount = reader.getUInt32(tagOffset + 4);
+
+                final long byteCount;
                 if (format == null) {
-                    // This error suggests that we are processing at an incorrect index and will generate
-                    // rubbish until we go out of bounds (which may be a while).  Exit now.
-                    handler.error("Invalid TIFF tag format code: " + formatCode);
-                    // TODO specify threshold as a parameter, or provide some other external control over this behaviour
-                    if (++invalidTiffFormatCodeCount > 5) {
-                        handler.error("Stopping processing as too many errors seen in TIFF IFD");
-                        return;
+                    Long byteCountOverride = handler.tryCustomProcessFormat(tagId, formatCode, componentCount);
+                    if (byteCountOverride == null) {
+                        // This error suggests that we are processing at an incorrect index and will generate
+                        // rubbish until we go out of bounds (which may be a while).  Exit now.
+                        handler.error(String.format("Invalid TIFF tag format code %d for tag 0x%04X", formatCode, tagId));
+                        // TODO specify threshold as a parameter, or provide some other external control over this behaviour
+                        if (++invalidTiffFormatCodeCount > 5) {
+                            handler.error("Stopping processing as too many errors seen in TIFF IFD");
+                            return;
+                        }
+                        continue;
                     }
-                    continue;
-                }
-
-                // 4 bytes dictate the number of components in this tag's data
-                final int componentCount = reader.getInt32(tagOffset + 4);
-                if (componentCount < 0) {
-                    handler.error("Negative TIFF tag component count");
-                    continue;
+                    byteCount = byteCountOverride;
+                } else {
+                    byteCount = componentCount * format.getComponentSizeBytes();
                 }
 
-                final int byteCount = componentCount * format.getComponentSizeBytes();
-
-                final int tagValueOffset;
+                final long tagValueOffset;
                 if (byteCount > 4) {
                     // If it's bigger than 4 bytes, the dir entry contains an offset.
-                    final int offsetVal = reader.getInt32(tagOffset + 8);
+                    final long offsetVal = reader.getUInt32(tagOffset + 8);
                     if (offsetVal + byteCount > reader.getLength()) {
                         // Bogus pointer offset and / or byteCount value
                         handler.error("Illegal TIFF tag pointer offset");
@@ -194,17 +204,23 @@ public class TiffReader
                     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);
+                // Some tags point to one or more additional IFDs to process
+                boolean isIfdPointer = false;
+                if (byteCount == 4 * componentCount) {
+                    for (int i = 0; i < componentCount; i++) {
+                        if (handler.tryEnterSubIfd(tagId)) {
+                            isIfdPointer = true;
+                            int subDirOffset = tiffHeaderOffset + reader.getInt32((int) (tagValueOffset + i * 4));
+                            processIfd(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset);
+                        }
                     }
                 }
+
+                // If it wasn't an IFD pointer, allow custom tag processing to occur
+                if (!isIfdPointer && !handler.customProcessTag((int) tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, (int) byteCount)) {
+                    // If no custom processing occurred, process the tag in the standard fashion
+                    processTag(handler, tagId, (int) tagValueOffset, (int) componentCount, formatCode, reader);
+                }
             }
 
             // at the end of each IFD is an optional link to the next IFD
@@ -228,6 +244,8 @@ public class TiffReader
             }
         } finally {
             handler.endingIFD();
+            if (resetByteOrder != null)
+                reader.setMotorolaByteOrder(resetByteOrder);
         }
     }
 
@@ -244,7 +262,7 @@ public class TiffReader
                 handler.setByteArray(tagId, reader.getBytes(tagValueOffset, componentCount));
                 break;
             case TiffDataFormat.CODE_STRING:
-                handler.setString(tagId, reader.getNullTerminatedString(tagValueOffset, componentCount));
+                handler.setString(tagId, reader.getNullTerminatedStringValue(tagValueOffset, componentCount, null));
                 break;
             case TiffDataFormat.CODE_RATIONAL_S:
                 if (componentCount == 1) {
@@ -349,7 +367,7 @@ public class TiffReader
                 }
                 break;
             default:
-                handler.error(String.format("Unknown format code %d for tag %d", formatCode, tagId));
+                handler.error(String.format("Invalid TIFF tag format code %d for tag 0x%04X", formatCode, tagId));
         }
     }
 
diff --git a/Source/com/drew/imaging/tiff/package-info.java b/Source/com/drew/imaging/tiff/package-info.java
new file mode 100644
index 0000000..0bd2d4f
--- /dev/null
+++ b/Source/com/drew/imaging/tiff/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with TIFF format files.
+ */
+package com.drew.imaging.tiff;
diff --git a/Source/com/drew/imaging/tiff/package.html b/Source/com/drew/imaging/tiff/package.html
deleted file mode 100644
index ce5b2e6..0000000
--- a/Source/com/drew/imaging/tiff/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/imaging/webp/WebpMetadataReader.java b/Source/com/drew/imaging/webp/WebpMetadataReader.java
new file mode 100644
index 0000000..4c2c191
--- /dev/null
+++ b/Source/com/drew/imaging/webp/WebpMetadataReader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.imaging.webp;
+
+import com.drew.imaging.riff.RiffProcessingException;
+import com.drew.imaging.riff.RiffReader;
+import com.drew.lang.StreamReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.file.FileMetadataReader;
+import com.drew.metadata.webp.WebpRiffHandler;
+
+import java.io.*;
+
+/**
+ * Obtains metadata from WebP files.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class WebpMetadataReader
+{
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file) throws IOException, RiffProcessingException
+    {
+        InputStream inputStream = new FileInputStream(file);
+        Metadata metadata;
+        try {
+            metadata = readMetadata(inputStream);
+        } finally {
+            inputStream.close();
+        }
+        new FileMetadataReader().read(file, metadata);
+        return metadata;
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream) throws IOException, RiffProcessingException
+    {
+        Metadata metadata = new Metadata();
+        new RiffReader().processRiff(new StreamReader(inputStream), new WebpRiffHandler(metadata));
+        return metadata;
+    }
+}
diff --git a/Source/com/drew/imaging/webp/package-info.java b/Source/com/drew/imaging/webp/package-info.java
new file mode 100644
index 0000000..18cf66f
--- /dev/null
+++ b/Source/com/drew/imaging/webp/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for working with WebP format files.
+ */
+package com.drew.imaging.webp;
diff --git a/Source/com/drew/lang/BufferBoundsException.java b/Source/com/drew/lang/BufferBoundsException.java
index b7ecab4..6045a0a 100644
--- a/Source/com/drew/lang/BufferBoundsException.java
+++ b/Source/com/drew/lang/BufferBoundsException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/lang/ByteArrayReader.java b/Source/com/drew/lang/ByteArrayReader.java
index 397d8ae..f5e58f0 100644
--- a/Source/com/drew/lang/ByteArrayReader.java
+++ b/Source/com/drew/lang/ByteArrayReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -38,34 +38,52 @@ public class ByteArrayReader extends RandomAccessReader
 {
     @NotNull
     private final byte[] _buffer;
+    private final int _baseOffset;
 
     @SuppressWarnings({ "ConstantConditions" })
     @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
     public ByteArrayReader(@NotNull byte[] buffer)
+    {
+        this(buffer, 0);
+    }
+
+    @SuppressWarnings({ "ConstantConditions" })
+    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
+    public ByteArrayReader(@NotNull byte[] buffer, int baseOffset)
     {
         if (buffer == null)
             throw new NullPointerException();
+        if (baseOffset < 0)
+            throw new IllegalArgumentException("Must be zero or greater");
 
         _buffer = buffer;
+        _baseOffset = baseOffset;
+    }
+
+    @Override
+    public int toUnshiftedOffset(int localOffset)
+    {
+        return localOffset + _baseOffset;
     }
 
     @Override
     public long getLength()
     {
-        return _buffer.length;
+        return _buffer.length - _baseOffset;
     }
 
     @Override
-    protected byte getByte(int index) throws IOException
+    public byte getByte(int index) throws IOException
     {
-        return _buffer[index];
+        validateIndex(index, 1);
+        return _buffer[index + _baseOffset];
     }
 
     @Override
     protected void validateIndex(int index, int bytesRequested) throws IOException
     {
         if (!isValidIndex(index, bytesRequested))
-            throw new BufferBoundsException(index, bytesRequested, _buffer.length);
+            throw new BufferBoundsException(toUnshiftedOffset(index), bytesRequested, _buffer.length);
     }
 
     @Override
@@ -73,7 +91,7 @@ public class ByteArrayReader extends RandomAccessReader
     {
         return bytesRequested >= 0
             && index >= 0
-            && (long)index + (long)bytesRequested - 1L < (long)_buffer.length;
+            && (long)index + (long)bytesRequested - 1L < getLength();
     }
 
     @Override
@@ -83,7 +101,7 @@ public class ByteArrayReader extends RandomAccessReader
         validateIndex(index, count);
 
         byte[] bytes = new byte[count];
-        System.arraycopy(_buffer, index, bytes, 0, count);
+        System.arraycopy(_buffer, index + _baseOffset, bytes, 0, count);
         return bytes;
     }
 }
diff --git a/Source/com/drew/lang/ByteConvert.java b/Source/com/drew/lang/ByteConvert.java
new file mode 100644
index 0000000..014e34b
--- /dev/null
+++ b/Source/com/drew/lang/ByteConvert.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ByteConvert
+{
+    public static int toInt32BigEndian(@NotNull byte[] bytes)
+    {
+        return (bytes[0] << 24 & 0xFF000000) |
+               (bytes[1] << 16 & 0xFF0000) |
+               (bytes[2] << 8  & 0xFF00) |
+               (bytes[3]       & 0xFF);
+    }
+
+    public static int toInt32LittleEndian(@NotNull byte[] bytes)
+    {
+        return (bytes[0]       & 0xFF) |
+               (bytes[1] << 8  & 0xFF00) |
+               (bytes[2] << 16 & 0xFF0000) |
+               (bytes[3] << 24 & 0xFF000000);
+    }
+}
diff --git a/Source/com/drew/lang/ByteTrie.java b/Source/com/drew/lang/ByteTrie.java
index 3855bda..68ba410 100644
--- a/Source/com/drew/lang/ByteTrie.java
+++ b/Source/com/drew/lang/ByteTrie.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/lang/Charsets.java b/Source/com/drew/lang/Charsets.java
new file mode 100644
index 0000000..dcb2efd
--- /dev/null
+++ b/Source/com/drew/lang/Charsets.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.lang;
+
+import java.nio.charset.Charset;
+
+/**
+ * Holds a set of commonly used character encodings.
+ *
+ * Newer JDKs include java.nio.charset.StandardCharsets, but we cannot use that in this library.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class Charsets
+{
+    public static final Charset UTF_8 = Charset.forName("UTF-8");
+    public static final Charset UTF_16 = Charset.forName("UTF-16");
+    public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+    public static final Charset ASCII = Charset.forName("US-ASCII");
+    public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
+    public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
+}
diff --git a/Source/com/drew/lang/CompoundException.java b/Source/com/drew/lang/CompoundException.java
index a7ed181..d065fec 100644
--- a/Source/com/drew/lang/CompoundException.java
+++ b/Source/com/drew/lang/CompoundException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/lang/DateUtil.java b/Source/com/drew/lang/DateUtil.java
new file mode 100644
index 0000000..56484df
--- /dev/null
+++ b/Source/com/drew/lang/DateUtil.java
@@ -0,0 +1,32 @@
+package com.drew.lang;
+
+/**
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class DateUtil
+{
+    private static int[] _daysInMonth365 = new int[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+    public static boolean isValidDate(int year, int month, int day)
+    {
+        if (year < 1 || year > 9999 || month < 0 || month > 11)
+            return false;
+
+        int daysInMonth = _daysInMonth365[month];
+        if (month == 1)
+        {
+            boolean isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+            if (isLeapYear)
+                daysInMonth++;
+        }
+
+        return day >= 1 && day <= daysInMonth;
+    }
+
+    public static boolean isValidTime(int hours, int minutes, int seconds)
+    {
+        return hours >= 0 && hours < 24
+            && minutes >= 0 && minutes < 60
+            && seconds >= 0 && seconds < 60;
+    }
+}
diff --git a/Source/com/drew/lang/GeoLocation.java b/Source/com/drew/lang/GeoLocation.java
index a006a59..a346484 100644
--- a/Source/com/drew/lang/GeoLocation.java
+++ b/Source/com/drew/lang/GeoLocation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -83,7 +83,7 @@ public final class GeoLocation
     {
         double[] dms = decimalToDegreesMinutesSeconds(decimal);
         DecimalFormat format = new DecimalFormat("0.##");
-        return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
+        return String.format("%s\u00B0 %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
     }
 
     /**
diff --git a/Source/com/drew/lang/KeyValuePair.java b/Source/com/drew/lang/KeyValuePair.java
index 313a611..bb22868 100644
--- a/Source/com/drew/lang/KeyValuePair.java
+++ b/Source/com/drew/lang/KeyValuePair.java
@@ -1,18 +1,39 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.lang;
 
 import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.StringValue;
 
 /**
- * Models a key/value pair, where both are non-null {@link String} objects.
+ * Models a key/value pair, where both are non-null {@link StringValue} objects.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
 public class KeyValuePair
 {
     private final String _key;
-    private final String _value;
+    private final StringValue _value;
 
-    public KeyValuePair(@NotNull String key, @NotNull String value)
+    public KeyValuePair(@NotNull String key, @NotNull StringValue value)
     {
         _key = key;
         _value = value;
@@ -25,7 +46,7 @@ public class KeyValuePair
     }
 
     @NotNull
-    public String getValue()
+    public StringValue getValue()
     {
         return _value;
     }
diff --git a/Source/com/drew/lang/NullOutputStream.java b/Source/com/drew/lang/NullOutputStream.java
index 3e9b583..4be4933 100644
--- a/Source/com/drew/lang/NullOutputStream.java
+++ b/Source/com/drew/lang/NullOutputStream.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/lang/RandomAccessFileReader.java b/Source/com/drew/lang/RandomAccessFileReader.java
index 76c5407..5f23fbe 100644
--- a/Source/com/drew/lang/RandomAccessFileReader.java
+++ b/Source/com/drew/lang/RandomAccessFileReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -39,17 +39,33 @@ public class RandomAccessFileReader extends RandomAccessReader
     private final long _length;
     private int _currentIndex;
 
+    private final int _baseOffset;
+
     @SuppressWarnings({ "ConstantConditions" })
     @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
     public RandomAccessFileReader(@NotNull RandomAccessFile file) throws IOException
+    {
+        this(file, 0);
+    }
+
+    @SuppressWarnings({ "ConstantConditions" })
+    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
+    public RandomAccessFileReader(@NotNull RandomAccessFile file, int baseOffset) throws IOException
     {
         if (file == null)
             throw new NullPointerException();
 
         _file = file;
+        _baseOffset = baseOffset;
         _length = _file.length();
     }
 
+    @Override
+    public int toUnshiftedOffset(int localOffset)
+    {
+        return localOffset + _baseOffset;
+    }
+
     @Override
     public long getLength()
     {
@@ -57,7 +73,7 @@ public class RandomAccessFileReader extends RandomAccessReader
     }
 
     @Override
-    protected byte getByte(int index) throws IOException
+    public byte getByte(int index) throws IOException
     {
         if (index != _currentIndex)
             seek(index);
diff --git a/Source/com/drew/lang/RandomAccessReader.java b/Source/com/drew/lang/RandomAccessReader.java
index 4e64865..7e5987a 100644
--- a/Source/com/drew/lang/RandomAccessReader.java
+++ b/Source/com/drew/lang/RandomAccessReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,9 +22,12 @@
 package com.drew.lang;
 
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 
 /**
  * Base class for random access data reading operations of common data types.
@@ -44,6 +47,8 @@ public abstract class RandomAccessReader
 {
     private boolean _isMotorolaByteOrder = true;
 
+    public abstract int toUnshiftedOffset(int localOffset);
+
     /**
      * Gets the byte value at the specified byte <code>index</code>.
      * <p>
@@ -52,11 +57,11 @@ public abstract class RandomAccessReader
      *
      * @param index The index from which to read the byte
      * @return The read byte value
-     * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative
+     * @throws IllegalArgumentException <code>index</code> is negative
      * @throws BufferBoundsException if the requested byte is beyond the end of the underlying data source
      * @throws IOException if the byte is unable to be read
      */
-    protected abstract byte getByte(int index) throws IOException;
+    public abstract byte getByte(int index) throws IOException;
 
     /**
      * Returns the required number of bytes from the specified index from the underlying source.
@@ -125,6 +130,24 @@ public abstract class RandomAccessReader
         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.
      *
@@ -197,6 +220,30 @@ public abstract class RandomAccessReader
         }
     }
 
+    /**
+     * 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.
      *
@@ -322,13 +369,19 @@ public abstract class RandomAccessReader
     }
 
     @NotNull
-    public String getString(int index, int bytesRequested) throws IOException
+    public StringValue getStringValue(int index, int bytesRequested, @Nullable Charset charset) throws IOException
     {
-        return new String(getBytes(index, bytesRequested));
+        return new StringValue(getBytes(index, bytesRequested), charset);
     }
 
     @NotNull
-    public String getString(int index, int bytesRequested, String charset) throws IOException
+    public String getString(int index, int bytesRequested, @NotNull Charset charset) throws IOException
+    {
+        return new String(getBytes(index, bytesRequested), charset.name());
+    }
+
+    @NotNull
+    public String getString(int index, int bytesRequested, @NotNull String charset) throws IOException
     {
         byte[] bytes = getBytes(index, bytesRequested);
         try {
@@ -349,17 +402,44 @@ public abstract class RandomAccessReader
      * @throws IOException The buffer does not contain enough bytes to satisfy this request.
      */
     @NotNull
-    public String getNullTerminatedString(int index, int maxLengthBytes) throws IOException
+    public String getNullTerminatedString(int index, int maxLengthBytes, @NotNull Charset charset) throws IOException
     {
-        // NOTE currently only really suited to single-byte character strings
+        return new String(getNullTerminatedBytes(index, maxLengthBytes), charset.name());
+    }
 
-        byte[] bytes = getBytes(index, maxLengthBytes);
+    @NotNull
+    public StringValue getNullTerminatedStringValue(int index, int maxLengthBytes, @Nullable Charset charset) throws IOException
+    {
+        byte[] bytes = getNullTerminatedBytes(index, maxLengthBytes);
+
+        return new StringValue(bytes, charset);
+    }
+
+    /**
+     * Returns the sequence of bytes punctuated by a <code>\0</code> value.
+     *
+     * @param index The index within the buffer at which to start reading the string.
+     * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
+     * the returned array will be <code>maxLengthBytes</code> long.
+     * @return The read byte array, excluding the null terminator.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public byte[] getNullTerminatedBytes(int index, int maxLengthBytes) throws IOException
+    {
+        byte[] buffer = getBytes(index, maxLengthBytes);
 
         // Count the number of non-null bytes
         int length = 0;
-        while (length < bytes.length && bytes[length] != '\0')
+        while (length < buffer.length && buffer[length] != 0)
             length++;
 
-        return new String(bytes, 0, length);
+        if (length == maxLengthBytes)
+            return buffer;
+
+        byte[] bytes = new byte[length];
+        if (length > 0)
+            System.arraycopy(buffer, 0, bytes, 0, length);
+        return bytes;
     }
 }
diff --git a/Source/com/drew/lang/RandomAccessStreamReader.java b/Source/com/drew/lang/RandomAccessStreamReader.java
index 5da26ed..e9c4056 100644
--- a/Source/com/drew/lang/RandomAccessStreamReader.java
+++ b/Source/com/drew/lang/RandomAccessStreamReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ import java.util.ArrayList;
  */
 public class RandomAccessStreamReader extends RandomAccessReader
 {
-    private final static int DEFAULT_CHUNK_LENGTH = 2 * 1024;
+    public final static int DEFAULT_CHUNK_LENGTH = 2 * 1024;
 
     @NotNull
     private final InputStream _stream;
@@ -41,15 +41,19 @@ public class RandomAccessStreamReader extends RandomAccessReader
     private final ArrayList<byte[]> _chunks = new ArrayList<byte[]>();
 
     private boolean _isStreamFinished;
-    private int _streamLength;
+    private long _streamLength;
 
     public RandomAccessStreamReader(@NotNull InputStream stream)
     {
-        this(stream, DEFAULT_CHUNK_LENGTH);
+        this(stream, DEFAULT_CHUNK_LENGTH, -1);
     }
 
-    @SuppressWarnings("ConstantConditions")
     public RandomAccessStreamReader(@NotNull InputStream stream, int chunkLength)
+    {
+        this(stream, chunkLength, -1);
+    }
+
+    public RandomAccessStreamReader(@NotNull InputStream stream, int chunkLength, long streamLength)
     {
         if (stream == null)
             throw new NullPointerException();
@@ -58,6 +62,7 @@ public class RandomAccessStreamReader extends RandomAccessReader
 
         _chunkLength = chunkLength;
         _stream = stream;
+        _streamLength = streamLength;
     }
 
     /**
@@ -69,6 +74,10 @@ public class RandomAccessStreamReader extends RandomAccessReader
     @Override
     public long getLength() throws IOException
     {
+        if (_streamLength != -1) {
+            return _streamLength;
+        }
+
         isValidIndex(Integer.MAX_VALUE, 1);
         assert(_isStreamFinished);
         return _streamLength;
@@ -77,7 +86,7 @@ public class RandomAccessStreamReader extends RandomAccessReader
     /**
      * Ensures that the buffered bytes extend to cover the specified index. If not, an attempt is made
      * to read to that point.
-     * <p/>
+     * <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
@@ -134,7 +143,12 @@ public class RandomAccessStreamReader extends RandomAccessReader
                 if (bytesRead == -1) {
                     // the stream has ended, which may be ok
                     _isStreamFinished = true;
-                    _streamLength = _chunks.size() * _chunkLength + totalBytesRead;
+                    int observedStreamLength = _chunks.size() * _chunkLength + totalBytesRead;
+                    if (_streamLength == -1) {
+                        _streamLength = observedStreamLength;
+                    } else if (_streamLength != observedStreamLength) {
+                        assert(false);
+                    }
 
                     // check we have enough bytes for the requested index
                     if (endIndex >= _streamLength) {
@@ -153,7 +167,13 @@ public class RandomAccessStreamReader extends RandomAccessReader
     }
 
     @Override
-    protected byte getByte(int index) throws IOException
+    public int toUnshiftedOffset(int localOffset)
+    {
+        return localOffset;
+    }
+
+    @Override
+    public byte getByte(int index) throws IOException
     {
         assert(index >= 0);
 
diff --git a/Source/com/drew/lang/Rational.java b/Source/com/drew/lang/Rational.java
index 5b80617..8a83002 100644
--- a/Source/com/drew/lang/Rational.java
+++ b/Source/com/drew/lang/Rational.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -35,7 +35,8 @@ import java.io.Serializable;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class Rational extends java.lang.Number implements Serializable
+@SuppressWarnings("WeakerAccess")
+public class Rational extends java.lang.Number implements Comparable<Rational>, Serializable
 {
     private static final long serialVersionUID = 510688928138848770L;
 
@@ -174,6 +175,12 @@ public class Rational extends java.lang.Number implements Serializable
                 (_denominator == 0 && _numerator == 0);
     }
 
+    /** Checks if either the numerator or denominator are zero. */
+    public boolean isZero()
+    {
+        return _numerator == 0 || _denominator == 0;
+    }
+
     /**
      * Returns a string representation of the object of form <code>numerator/denominator</code>.
      *
@@ -211,16 +218,47 @@ public class Rational extends java.lang.Number implements Serializable
     }
 
     /**
-     * Decides whether a brute-force simplification calculation should be avoided
-     * by comparing the maximum number of possible calculations with some threshold.
+     * Compares two {@link Rational} instances, returning true if they are mathematically
+     * equivalent (in consistence with {@link Rational#equals(Object)} method).
      *
-     * @return true if the simplification should be performed, otherwise false
+     * @param that the {@link Rational} to compare this instance to.
+     * @return the value {@code 0} if this {@link Rational} is
+     *         equal to the argument {@link Rational} mathematically; a value less
+     *         than {@code 0} if this {@link Rational} is less
+     *         than the argument {@link Rational}; and a value greater
+     *         than {@code 0} if this {@link Rational} is greater than the argument
+     *         {@link Rational}.
      */
-    private boolean tooComplexForSimplification()
-    {
-        double maxPossibleCalculations = (((double) (Math.min(_denominator, _numerator) - 1) / 5d) + 2);
-        final int maxSimplificationCalculations = 1000;
-        return maxPossibleCalculations > maxSimplificationCalculations;
+    public int compareTo(@NotNull Rational that) {
+        return Double.compare(this.doubleValue(), that.doubleValue());
+    }
+
+    /**
+     * Indicates whether this instance and <code>other</code> are numerically equal,
+     * even if their representations differ.
+     *
+     * For example, 1/2 is equal to 10/20 by this method.
+     * Similarly, 1/0 is equal to 100/0 by this method.
+     * To test equal representations, use EqualsExact.
+     *
+     * @param other The rational value to compare with
+     */
+    public boolean equals(Rational other) {
+        return other.doubleValue() == doubleValue();
+    }
+
+    /**
+     * Indicates whether this instance and <code>other</code> have identical
+     * Numerator and Denominator.
+     * <p>
+     * For example, 1/2 is not equal to 10/20 by this method.
+     * Similarly, 1/0 is not equal to 100/0 by this method.
+     * To test numerically equivalence, use Equals(Rational).</p>
+     *
+     * @param other The rational value to compare with
+     */
+    public boolean equalsExact(Rational other) {
+        return getDenominator() == other.getDenominator() && getNumerator() == other.getNumerator();
     }
 
     /**
@@ -248,49 +286,38 @@ public class Rational extends java.lang.Number implements Serializable
 
     /**
      * <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>
+     * Simplifies the representation of this {@link Rational} number.</p>
      * <p>
-     * However, generating the prime number series seems to be a hefty task.  Perhaps
-     * it's simpler to check if both d &amp; n are divisible by all numbers from 2 {@literal ->}
-     * (Math.min(denominator, numerator) / 2).  In doing this, one can check for 2
-     * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5.
-     * This leaves four numbers from every ten to check.</p>
+     * For example, 5/10 simplifies to 1/2 because both Numerator
+     * and Denominator share a common factor of 5.</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
+     * Uses the Euclidean Algorithm to find the greatest common divisor.</p>
      *
-     *   Math.min(denominator, numerator) - 1
-     * = ------------------------------------ + 2
-     *                  5
-     * </code></pre>
-     *
-     * @return a simplified instance, or if the Rational could not be simplified,
-     *         returns itself (unchanged)
+     * @return A simplified instance if one exists, otherwise a copy of the original value.
      */
     @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);
-            }
+        long gcd = GCD(_numerator, _denominator);
+
+        return new Rational(_numerator / gcd, _denominator / gcd);
+    }
+
+    private static long GCD(long a, long b)
+    {
+        if (a < 0)
+            a = -a;
+        if (b < 0)
+            b = -b;
+
+        while (a != 0 && b != 0)
+        {
+            if (a > b)
+                a %= b;
+            else
+                b %= a;
         }
-        return this;
+
+        return a == 0 ? b : a;
     }
 }
diff --git a/Source/com/drew/lang/SequentialByteArrayReader.java b/Source/com/drew/lang/SequentialByteArrayReader.java
index 3b18c84..846efeb 100644
--- a/Source/com/drew/lang/SequentialByteArrayReader.java
+++ b/Source/com/drew/lang/SequentialByteArrayReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -36,18 +36,29 @@ public class SequentialByteArrayReader extends SequentialReader
     private final byte[] _bytes;
     private int _index;
 
-    @SuppressWarnings("ConstantConditions")
+    @Override
+    public long getPosition()
+    {
+        return _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 = 0;
+        _index = baseIndex;
     }
 
     @Override
-    protected byte getByte() throws IOException
+    public byte getByte() throws IOException
     {
         if (_index >= _bytes.length) {
             throw new EOFException("End of data reached.");
@@ -70,6 +81,17 @@ public class SequentialByteArrayReader extends SequentialReader
         return bytes;
     }
 
+    @Override
+    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
+    {
+        if (_index + count > _bytes.length) {
+            throw new EOFException("End of data reached.");
+        }
+
+        System.arraycopy(_bytes, _index, buffer, offset, count);
+        _index += count;
+    }
+
     @Override
     public void skip(long n) throws IOException
     {
@@ -100,4 +122,9 @@ public class SequentialByteArrayReader extends SequentialReader
 
         return true;
     }
+
+    @Override
+    public int available() {
+        return _bytes.length - _index;
+    }
 }
diff --git a/Source/com/drew/lang/SequentialReader.java b/Source/com/drew/lang/SequentialReader.java
index a1bcdb5..d172aa9 100644
--- a/Source/com/drew/lang/SequentialReader.java
+++ b/Source/com/drew/lang/SequentialReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,26 +22,32 @@
 package com.drew.lang;
 
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public abstract class SequentialReader
 {
     // TODO review whether the masks are needed (in both this and RandomAccessReader)
 
     private boolean _isMotorolaByteOrder = true;
 
+    public abstract long getPosition() throws IOException;
+
     /**
      * Gets the next byte in the sequence.
      *
      * @return The read byte value
      */
-    protected abstract byte getByte() throws IOException;
+    public abstract byte getByte() throws IOException;
 
     /**
      * Returns the required number of bytes from the sequence.
@@ -52,6 +58,14 @@ public abstract class SequentialReader
     @NotNull
     public abstract byte[] getBytes(int count) throws IOException;
 
+    /**
+     * Retrieves bytes, writing them into a caller-provided buffer.
+     * @param buffer The array to write bytes to.
+     * @param offset The starting position within buffer to write to.
+     * @param count The number of bytes to be written.
+     */
+    public abstract void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException;
+
     /**
      * Skips forward in the sequence. If the sequence ends, an {@link EOFException} is thrown.
      *
@@ -70,6 +84,24 @@ public abstract class SequentialReader
      */
     public abstract boolean trySkip(long n) throws IOException;
 
+    /**
+     * Returns an estimate of the number of bytes that can be read (or skipped
+     * over) from this {@link SequentialReader} without blocking by the next
+     * invocation of a method for this input stream. A single read or skip of
+     * this many bytes will not block, but may read or skip fewer bytes.
+     * <p>
+     * Note that while some implementations of {@link SequentialReader} like
+     * {@link SequentialByteArrayReader} will return the total remaining number
+     * of bytes in the stream, others will not. It is never correct to use the
+     * return value of this method to allocate a buffer intended to hold all
+     * data in this stream.
+     *
+     * @return an estimate of the number of bytes that can be read (or skipped
+     *         over) from this {@link SequentialReader} without blocking or
+     *         {@code 0} when it reaches the end of the input stream.
+     */
+    public abstract int available();
+
     /**
      * Sets the endianness of this reader.
      * <ul>
@@ -283,6 +315,19 @@ public abstract class SequentialReader
         }
     }
 
+    @NotNull
+    public String getString(int bytesRequested, @NotNull Charset charset) throws IOException
+    {
+        byte[] bytes = getBytes(bytesRequested);
+        return new String(bytes, charset);
+    }
+
+    @NotNull
+    public StringValue getStringValue(int bytesRequested, @Nullable Charset charset) throws IOException
+    {
+        return new StringValue(getBytes(bytesRequested), charset);
+    }
+
     /**
      * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
      *
@@ -292,17 +337,53 @@ public abstract class SequentialReader
      * @throws IOException The buffer does not contain enough bytes to satisfy this request.
      */
     @NotNull
-    public String getNullTerminatedString(int maxLengthBytes) throws IOException
+    public String getNullTerminatedString(int maxLengthBytes, Charset charset) throws IOException
+    {
+       return getNullTerminatedStringValue(maxLengthBytes, charset).toString();
+    }
+
+    /**
+     * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
+     *
+     * @param maxLengthBytes The maximum number of bytes to read.  If a <code>\0</code> byte is not reached within this limit,
+     *                       reading will stop and the string will be truncated to this length.
+     * @param charset The <code>Charset</code> to register with the returned <code>StringValue</code>, or <code>null</code> if the encoding
+     *                is unknown
+     * @return The read string.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public StringValue getNullTerminatedStringValue(int maxLengthBytes, Charset charset) throws IOException
     {
-        // NOTE currently only really suited to single-byte character strings
+        byte[] bytes = getNullTerminatedBytes(maxLengthBytes);
 
-        byte[] bytes = new byte[maxLengthBytes];
+        return new StringValue(bytes, charset);
+    }
+
+    /**
+     * Returns the sequence of bytes punctuated by a <code>\0</code> value.
+     *
+     * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
+     * the returned array will be <code>maxLengthBytes</code> long.
+     * @return The read byte array, excluding the null terminator.
+     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    public byte[] getNullTerminatedBytes(int maxLengthBytes) throws IOException
+    {
+        byte[] buffer = new byte[maxLengthBytes];
 
         // Count the number of non-null bytes
         int length = 0;
-        while (length < bytes.length && (bytes[length] = getByte()) != '\0')
+        while (length < buffer.length && (buffer[length] = getByte()) != 0)
             length++;
 
-        return new String(bytes, 0, length);
+        if (length == maxLengthBytes)
+            return buffer;
+
+        byte[] bytes = new byte[length];
+        if (length > 0)
+            System.arraycopy(buffer, 0, bytes, 0, length);
+        return bytes;
     }
 }
diff --git a/Source/com/drew/lang/StreamReader.java b/Source/com/drew/lang/StreamReader.java
index c55db50..1e30b12 100644
--- a/Source/com/drew/lang/StreamReader.java
+++ b/Source/com/drew/lang/StreamReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -36,6 +36,14 @@ public class StreamReader extends SequentialReader
     @NotNull
     private final InputStream _stream;
 
+    private long _pos;
+
+    @Override
+    public long getPosition()
+    {
+        return _pos;
+    }
+
     @SuppressWarnings("ConstantConditions")
     public StreamReader(@NotNull InputStream stream)
     {
@@ -43,14 +51,16 @@ public class StreamReader extends SequentialReader
             throw new NullPointerException();
 
         _stream = stream;
+        _pos = 0;
     }
 
     @Override
-    protected byte getByte() throws IOException
+    public byte getByte() throws IOException
     {
         int value = _stream.read();
         if (value == -1)
             throw new EOFException("End of data reached.");
+        _pos++;
         return (byte)value;
     }
 
@@ -59,17 +69,23 @@ public class StreamReader extends SequentialReader
     public byte[] getBytes(int count) throws IOException
     {
         byte[] bytes = new byte[count];
-        int totalBytesRead = 0;
+        getBytes(bytes, 0, count);
+        return bytes;
+    }
 
-        while (totalBytesRead != count) {
-            final int bytesRead = _stream.read(bytes, totalBytesRead, count - totalBytesRead);
+    @Override
+    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
+    {
+        int totalBytesRead = 0;
+        while (totalBytesRead != count)
+        {
+            final int bytesRead = _stream.read(buffer, offset + totalBytesRead, count - totalBytesRead);
             if (bytesRead == -1)
                 throw new EOFException("End of data reached.");
             totalBytesRead += bytesRead;
             assert(totalBytesRead <= count);
         }
-
-        return bytes;
+        _pos += totalBytesRead;
     }
 
     @Override
@@ -93,6 +109,15 @@ public class StreamReader extends SequentialReader
         return skipInternal(n) == n;
     }
 
+    @Override
+    public int available() {
+        try {
+            return _stream.available();
+        } catch (IOException e) {
+            return 0;
+        }
+    }
+
     private long skipInternal(long n) throws IOException
     {
         // It seems that for some streams, such as BufferedInputStream, that skip can return
@@ -109,6 +134,7 @@ public class StreamReader extends SequentialReader
             if (skipped == 0)
                 break;
         }
+        _pos += skippedTotal;
         return skippedTotal;
     }
 }
diff --git a/Source/com/drew/lang/StreamUtil.java b/Source/com/drew/lang/StreamUtil.java
new file mode 100644
index 0000000..d8dea16
--- /dev/null
+++ b/Source/com/drew/lang/StreamUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.lang;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class StreamUtil
+{
+    public static byte[] readAllBytes(InputStream stream) throws IOException
+    {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+        byte[] buffer = new byte[1024];
+        while (true) {
+            int bytesRead = stream.read(buffer);
+            if (bytesRead == -1)
+                break;
+            outputStream.write(buffer, 0, bytesRead);
+        }
+
+        return outputStream.toByteArray();
+    }
+}
diff --git a/Source/com/drew/lang/StringUtil.java b/Source/com/drew/lang/StringUtil.java
index 35c3a9d..9423c35 100644
--- a/Source/com/drew/lang/StringUtil.java
+++ b/Source/com/drew/lang/StringUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -33,7 +33,7 @@ import java.util.Iterator;
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
-public class StringUtil
+public final class StringUtil
 {
     @NotNull
     public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter)
diff --git a/Source/com/drew/lang/annotations/NotNull.java b/Source/com/drew/lang/annotations/NotNull.java
index 4bba728..0c48378 100644
--- a/Source/com/drew/lang/annotations/NotNull.java
+++ b/Source/com/drew/lang/annotations/NotNull.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/lang/annotations/Nullable.java b/Source/com/drew/lang/annotations/Nullable.java
index e37e45f..017d280 100644
--- a/Source/com/drew/lang/annotations/Nullable.java
+++ b/Source/com/drew/lang/annotations/Nullable.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/lang/annotations/package-info.java b/Source/com/drew/lang/annotations/package-info.java
new file mode 100644
index 0000000..5e2f5a5
--- /dev/null
+++ b/Source/com/drew/lang/annotations/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA
+ * to provide design-time warnings about potential run-time errors.
+ */
+package com.drew.lang.annotations;
diff --git a/Source/com/drew/lang/annotations/package.html b/Source/com/drew/lang/annotations/package.html
deleted file mode 100644
index c648601..0000000
--- a/Source/com/drew/lang/annotations/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/lang/package-info.java b/Source/com/drew/lang/package-info.java
new file mode 100644
index 0000000..6c4e041
--- /dev/null
+++ b/Source/com/drew/lang/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes of generic utility.
+ */
+package com.drew.lang;
diff --git a/Source/com/drew/lang/package.html b/Source/com/drew/lang/package.html
deleted file mode 100644
index aa6d0d0..0000000
--- a/Source/com/drew/lang/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/metadata/Age.java b/Source/com/drew/metadata/Age.java
index 4022214..03d6cd6 100644
--- a/Source/com/drew/metadata/Age.java
+++ b/Source/com/drew/metadata/Age.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -50,9 +50,6 @@ public class Age
     @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;
 
@@ -142,7 +139,7 @@ public class Age
     }
 
     @Override
-    public boolean equals(Object o)
+    public boolean equals(@Nullable Object o)
     {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
diff --git a/Source/com/drew/metadata/Directory.java b/Source/com/drew/metadata/Directory.java
index 44f5628..5ab379a 100644
--- a/Source/com/drew/metadata/Directory.java
+++ b/Source/com/drew/metadata/Directory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -28,9 +28,12 @@ import com.drew.lang.annotations.SuppressWarnings;
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
 import java.text.DateFormat;
+import java.text.DecimalFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Abstract base class for all directory implementations, having methods for getting and setting tag values of various
@@ -38,8 +41,11 @@ import java.util.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@java.lang.SuppressWarnings("WeakerAccess")
 public abstract class Directory
 {
+    private static final String _floatFormatPattern = "0.###";
+
     /** Map of values hashed by type identifiers. */
     @NotNull
     protected final Map<Integer, Object> _tagMap = new HashMap<Integer, Object>();
@@ -58,6 +64,9 @@ public abstract class Directory
     /** The descriptor used to interpret tag values. */
     protected TagDescriptor _descriptor;
 
+    @Nullable
+    private Directory _parent;
+
 // ABSTRACT METHODS
 
     /**
@@ -81,6 +90,14 @@ public abstract class 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.
      *
@@ -164,6 +181,17 @@ public abstract class Directory
         return _errorList.size();
     }
 
+    @Nullable
+    public Directory getParent()
+    {
+        return _parent;
+    }
+
+    public void setParent(@NotNull Directory parent)
+    {
+        _parent = parent;
+    }
+
 // TAG SETTERS
 
     /**
@@ -232,6 +260,20 @@ public abstract class Directory
         setObjectArray(tagType, doubles);
     }
 
+    /**
+     * Sets a <code>StringValue</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a StringValue
+     */
+    @java.lang.SuppressWarnings({ "ConstantConditions" })
+    public void setStringValue(int tagType, @NotNull StringValue value)
+    {
+        if (value == null)
+            throw new NullPointerException("cannot set a null StringValue");
+        setObject(tagType, value);
+    }
+
     /**
      * Sets a <code>String</code> value for the specified tag.
      *
@@ -257,6 +299,17 @@ public abstract class Directory
         setObjectArray(tagType, strings);
     }
 
+    /**
+     * Sets a <code>StringValue[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param strings the StringValue array to store
+     */
+    public void setStringValueArray(int tagType, @NotNull StringValue[] strings)
+    {
+        setObjectArray(tagType, strings);
+    }
+
     /**
      * Sets a <code>boolean</code> value for the specified tag.
      *
@@ -413,12 +466,12 @@ public abstract class Directory
 
         if (o instanceof Number) {
             return ((Number)o).intValue();
-        } else if (o instanceof String) {
+        } else if (o instanceof String || o instanceof StringValue) {
             try {
-                return Integer.parseInt((String)o);
+                return Integer.parseInt(o.toString());
             } catch (NumberFormatException nfe) {
                 // convert the char array to an int
-                String s = (String)o;
+                String s = o.toString();
                 byte[] bytes = s.getBytes();
                 long val = 0;
                 for (byte aByte : bytes) {
@@ -439,13 +492,17 @@ public abstract class Directory
             int[] ints = (int[])o;
             if (ints.length == 1)
                 return ints[0];
+        } else if (o instanceof short[]) {
+            short[] shorts = (short[])o;
+            if (shorts.length == 1)
+                return (int)shorts[0];
         }
         return null;
     }
 
     /**
      * Gets the specified tag's value as a String array, if possible.  Only supported
-     * where the tag is set as String[], String, int[], byte[] or Rational[].
+     * where the tag is set as StringValue[], String[], StringValue, String, int[], byte[] or Rational[].
      *
      * @param tagType the tag identifier
      * @return the tag's value as an array of Strings. If the value is unset or cannot be converted, <code>null</code> is returned.
@@ -460,19 +517,30 @@ public abstract class Directory
             return (String[])o;
         if (o instanceof String)
             return new String[] { (String)o };
+        if (o instanceof StringValue)
+            return new String[] { o.toString() };
+        if (o instanceof StringValue[]) {
+            StringValue[] stringValues = (StringValue[])o;
+            String[] strings = new String[stringValues.length];
+            for (int i = 0; i < strings.length; i++)
+                strings[i] = stringValues[i].toString();
+            return strings;
+        }
         if (o instanceof int[]) {
             int[] ints = (int[])o;
             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[]) {
+        }
+        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[]) {
+        }
+        if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
             String[] strings = new String[rationals.length];
             for (int i = 0; i < strings.length; i++)
@@ -482,6 +550,26 @@ public abstract class Directory
         return null;
     }
 
+    /**
+     * Gets the specified tag's value as a StringValue array, if possible.
+     * Only succeeds if the tag is set as StringValue[], or StringValue.
+     *
+     * @param tagType the tag identifier
+     * @return the tag's value as an array of StringValues. If the value is unset or cannot be converted, <code>null</code> is returned.
+     */
+    @Nullable
+    public StringValue[] getStringValueArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof StringValue[])
+            return (StringValue[])o;
+        if (o instanceof StringValue)
+            return new StringValue[] {(StringValue) o};
+        return null;
+    }
+
     /**
      * 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[].
@@ -548,6 +636,8 @@ public abstract class Directory
         Object o = getObject(tagType);
         if (o == null) {
             return null;
+        } else if (o instanceof StringValue) {
+            return ((StringValue)o).getBytes();
         } else if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
             byte[] bytes = new byte[rationals.length];
@@ -603,9 +693,9 @@ public abstract class Directory
         Object o = getObject(tagType);
         if (o == null)
             return null;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Double.parseDouble((String)o);
+                return Double.parseDouble(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
             }
@@ -635,9 +725,9 @@ public abstract class Directory
         Object o = getObject(tagType);
         if (o == null)
             return null;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Float.parseFloat((String)o);
+                return Float.parseFloat(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
             }
@@ -651,7 +741,7 @@ public abstract class Directory
     public long getLong(int tagType) throws MetadataException
     {
         Long value = getLongObject(tagType);
-        if (value!=null)
+        if (value != null)
             return value;
         Object o = getObject(tagType);
         if (o == null)
@@ -666,9 +756,9 @@ public abstract class Directory
         Object o = getObject(tagType);
         if (o == null)
             return null;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Long.parseLong((String)o);
+                return Long.parseLong(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
             }
@@ -682,7 +772,7 @@ public abstract class Directory
     public boolean getBoolean(int tagType) throws MetadataException
     {
         Boolean value = getBooleanObject(tagType);
-        if (value!=null)
+        if (value != null)
             return value;
         Object o = getObject(tagType);
         if (o == null)
@@ -700,9 +790,9 @@ public abstract class Directory
             return null;
         if (o instanceof Boolean)
             return (Boolean)o;
-        if (o instanceof String) {
+        if (o instanceof String || o instanceof StringValue) {
             try {
-                return Boolean.getBoolean((String)o);
+                return Boolean.getBoolean(o.toString());
             } catch (NumberFormatException nfe) {
                 return null;
             }
@@ -716,12 +806,12 @@ public abstract class Directory
      * 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.
+     * the GMT {@link TimeZone}.  If the {@link TimeZone} is known, call the overload that accepts one as an argument.
      */
     @Nullable
     public java.util.Date getDate(int tagType)
     {
-        return getDate(tagType, null);
+        return getDate(tagType, null, null);
     }
 
     /**
@@ -729,21 +819,41 @@ public abstract class Directory
      * <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.
+     * is only considered if the underlying value is a string and it has no time zone information, otherwise it has no effect.
      */
     @Nullable
     public java.util.Date getDate(int tagType, @Nullable TimeZone timeZone)
     {
-        Object o = getObject(tagType);
+        return getDate(tagType, null, timeZone);
+    }
 
-        if (o == null)
-            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 {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).  Note that this parameter
+     * is only considered if the underlying value is a string and it has no time zone information, otherwise it has no effect.
+     * In addition, the {@code subsecond} parameter, which specifies the number of digits after the decimal point in the seconds,
+     * is set to the returned Date. This parameter is only considered if the underlying value is a string and is has
+     * no subsecond information, otherwise it has no effect.
+     *
+     * @param tagType the tag identifier
+     * @param subsecond the subsecond value for the Date
+     * @param timeZone the time zone to use
+     * @return a Date representing the time value
+     */
+    @Nullable
+    public java.util.Date getDate(int tagType, @Nullable String subsecond, @Nullable TimeZone timeZone)
+    {
+        Object o = getObject(tagType);
 
         if (o instanceof java.util.Date)
             return (java.util.Date)o;
 
-        if (o instanceof String) {
-            // This seems to cover all known Exif date strings
+        java.util.Date date = null;
+
+        if ((o instanceof String) || (o instanceof StringValue)) {
+            // This seems to cover all known Exif and Xmp date strings
             // Note that "    :  :     :  :  " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
             String datePatterns[] = {
                     "yyyy:MM:dd HH:mm:ss",
@@ -751,20 +861,66 @@ public abstract class Directory
                     "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;
+                    "yyyy.MM.dd HH:mm",
+                    "yyyy-MM-dd'T'HH:mm:ss",
+                    "yyyy-MM-dd'T'HH:mm",
+                    "yyyy-MM-dd",
+                    "yyyy-MM",
+                    "yyyyMMdd", // as used in IPTC data
+                    "yyyy" };
+
+            String dateString = o.toString();
+
+            // if the date string has subsecond information, it supersedes the subsecond parameter
+            Pattern subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)");
+            Matcher subsecondMatcher = subsecondPattern.matcher(dateString);
+            if (subsecondMatcher.find()) {
+                subsecond = subsecondMatcher.group(2).substring(1);
+                dateString = subsecondMatcher.replaceAll("$1");
+            }
+
+            // if the date string has time zone information, it supersedes the timeZone parameter
+            Pattern timeZonePattern = Pattern.compile("(Z|[+-]\\d\\d:\\d\\d)$");
+            Matcher timeZoneMatcher = timeZonePattern.matcher(dateString);
+            if (timeZoneMatcher.find()) {
+                timeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replaceAll("Z", ""));
+                dateString = timeZoneMatcher.replaceAll("");
+            }
+
             for (String datePattern : datePatterns) {
                 try {
                     DateFormat parser = new SimpleDateFormat(datePattern);
                     if (timeZone != null)
                         parser.setTimeZone(timeZone);
-                    return parser.parse(dateString);
+                    else
+                        parser.setTimeZone(TimeZone.getTimeZone("GMT")); // don't interpret zone time
+
+                    date = parser.parse(dateString);
+                    break;
                 } catch (ParseException ex) {
                     // simply try the next pattern
                 }
             }
         }
-        return null;
+
+        if (date == null)
+            return null;
+
+        if (subsecond == null)
+            return date;
+
+        try {
+            int millisecond = (int) (Double.parseDouble("." + subsecond) * 1000);
+            if (millisecond >= 0 && millisecond < 1000) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(date);
+                calendar.set(Calendar.MILLISECOND, millisecond);
+                return calendar.getTime();
+            }
+            return date;
+        } catch (NumberFormatException e) {
+            return date;
+        }
     }
 
     /** Returns the specified tag's value as a Rational.  If the value is unset or cannot be converted, <code>null</code> is returned. */
@@ -823,37 +979,65 @@ public abstract class Directory
             // 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)
+
+            if (Object.class.isAssignableFrom(componentType)) {
+                // object array
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.get(o, i).toString());
-                else if (isIntArray)
+                }
+            } else if (componentType.getName().equals("int")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.getInt(o, i));
-                else if (isShortArray)
+                }
+            } else if (componentType.getName().equals("short")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.getShort(o, i));
-                else if (isLongArray)
+                }
+            } else if (componentType.getName().equals("long")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
                     string.append(Array.getLong(o, i));
-                else if (isFloatArray)
-                    string.append(Array.getFloat(o, i));
-                else if (isDoubleArray)
-                    string.append(Array.getDouble(o, i));
-                else if (isByteArray)
-                    string.append(Array.getByte(o, i));
-                else
-                    addError("Unexpected array component type: " + componentType.getName());
+                }
+            } else if (componentType.getName().equals("float")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
+                    string.append(new DecimalFormat(_floatFormatPattern).format(Array.getFloat(o, i)));
+                }
+            } else if (componentType.getName().equals("double")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
+                    string.append(new DecimalFormat(_floatFormatPattern).format(Array.getDouble(o, i)));
+                }
+            } else if (componentType.getName().equals("byte")) {
+                for (int i = 0; i < arrayLength; i++) {
+                    if (i != 0)
+                        string.append(' ');
+                    string.append(Array.getByte(o, i) & 0xff);
+                }
+            } else {
+                addError("Unexpected array component type: " + componentType.getName());
             }
+
             return string.toString();
         }
 
+        if (o instanceof Double)
+            return new DecimalFormat(_floatFormatPattern).format(((Double)o).doubleValue());
+
+        if (o instanceof Float)
+            return new DecimalFormat(_floatFormatPattern).format(((Float)o).floatValue());
+
         // 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.
@@ -874,6 +1058,15 @@ public abstract class Directory
         }
     }
 
+    @Nullable
+    public StringValue getStringValue(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o instanceof StringValue)
+            return (StringValue)o;
+        return null;
+    }
+
     /**
      * Returns the object hashed for the particular tag type specified, if available.
      *
diff --git a/Source/com/drew/metadata/DefaultTagDescriptor.java b/Source/com/drew/metadata/ErrorDirectory.java
similarity index 51%
rename from Source/com/drew/metadata/DefaultTagDescriptor.java
rename to Source/com/drew/metadata/ErrorDirectory.java
index ed157bc..7412421 100644
--- a/Source/com/drew/metadata/DefaultTagDescriptor.java
+++ b/Source/com/drew/metadata/ErrorDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,31 +21,55 @@
 package com.drew.metadata;
 
 import com.drew.lang.annotations.NotNull;
+import java.util.*;
 
 /**
- * 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.
+ * A directory to use for the reporting of errors. No values may be added to this directory, only warnings and errors.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class DefaultTagDescriptor extends TagDescriptor<Directory>
+
+public final class ErrorDirectory extends Directory
 {
-    public DefaultTagDescriptor(@NotNull Directory directory)
+
+    public ErrorDirectory()
+    {}
+
+    public ErrorDirectory(String error)
     {
-        super(directory);
+        super.addError(error);
     }
 
-    /**
-     * 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.
-     */
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Error";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return new HashMap<Integer, String>();
+    }
+
+    @Override
     @NotNull
     public String getTagName(int tagType)
     {
-        String hex = Integer.toHexString(tagType).toUpperCase();
-        while (hex.length() < 4) hex = "0" + hex;
-        return "Unknown tag 0x" + hex;
+        return "";
+    }
+
+    @Override
+    public boolean hasTagName(int tagType)
+    {
+        return false;
+    }
+
+    @Override
+    public void setObject(int tagType, @NotNull Object value)
+    {
+        throw new UnsupportedOperationException(String.format("Cannot add value to %s.", ErrorDirectory.class.getName()));
     }
 }
diff --git a/Source/com/drew/metadata/Face.java b/Source/com/drew/metadata/Face.java
index 1cf5a4a..4178adb 100644
--- a/Source/com/drew/metadata/Face.java
+++ b/Source/com/drew/metadata/Face.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/metadata/Metadata.java b/Source/com/drew/metadata/Metadata.java
index 425be42..d8c9e25 100644
--- a/Source/com/drew/metadata/Metadata.java
+++ b/Source/com/drew/metadata/Metadata.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -35,97 +35,88 @@ import java.util.*;
  */
 public final class Metadata
 {
-    @NotNull
-    private final Map<Class<? extends Directory>,Directory> _directoryByClass = new HashMap<Class<? extends Directory>, Directory>();
-
     /**
-     * List of Directory objects set against this object.  Keeping a list handy makes
-     * creation of an Iterator and counting tags simple.
+     * The list of {@link Directory} instances in this container, in the order they were added.
      */
     @NotNull
-    private final Collection<Directory> _directoryList = new ArrayList<Directory>();
+    private final List<Directory> _directories = new ArrayList<Directory>();
 
     /**
-     * Returns an objects for iterating over Directory objects in the order in which they were added.
+     * Returns an iterable set of the {@link Directory} instances contained in this metadata collection.
      *
-     * @return an iterable collection of directories
+     * @return an iterable set of directories
      */
     @NotNull
     public Iterable<Directory> getDirectories()
     {
-        return Collections.unmodifiableCollection(_directoryList);
+        return _directories;
+    }
+
+    @NotNull
+    @SuppressWarnings("unchecked")
+    public <T extends Directory> Collection<T> getDirectoriesOfType(Class<T> type)
+    {
+        List<T> directories = new ArrayList<T>();
+        for (Directory dir : _directories) {
+            if (type.isAssignableFrom(dir.getClass())) {
+                directories.add((T)dir);
+            }
+        }
+        return directories;
     }
 
     /**
-     * Returns a count of unique directories in this metadata collection.
+     * Returns the count of directories in this metadata collection.
      *
      * @return the number of unique directory types set for this metadata collection
      */
     public int getDirectoryCount()
     {
-        return _directoryList.size();
+        return _directories.size();
     }
 
     /**
-     * Returns a {@link Directory} of specified type.  If this {@link Metadata} object already contains
-     * such a directory, it is returned.  Otherwise a new instance of this directory will be created and stored within
-     * this {@link Metadata} object.
+     * Adds a directory to this metadata collection.
      *
-     * @param type the type of the Directory implementation required.
-     * @return a directory of the specified type.
+     * @param directory the {@link Directory} to add into this metadata collection.
      */
-    @NotNull
-    @SuppressWarnings("unchecked")
-    public <T extends Directory> T getOrCreateDirectory(@NotNull Class<T> type)
+    public <T extends Directory> void addDirectory(@NotNull T directory)
     {
-        // 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.
-
-        // check if we've already issued this type of directory
-        if (_directoryByClass.containsKey(type))
-            return (T)_directoryByClass.get(type);
-
-        T directory;
-        try {
-            directory = type.newInstance();
-        } catch (Exception e) {
-            throw new RuntimeException("Cannot instantiate provided Directory type: " + type.toString());
-        }
-        // store the directory
-        _directoryByClass.put(type, directory);
-        _directoryList.add(directory);
-
-        return directory;
+        _directories.add(directory);
     }
 
     /**
-     * If this {@link Metadata} object contains a {@link Directory} of the specified type, it is returned.
-     * Otherwise <code>null</code> is returned.
+     * 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 a Directory of type T if it exists in this {@link Metadata} object, otherwise <code>null</code>.
+     * @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 getDirectory(@NotNull Class<T> type)
+    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.
-
-        return (T)_directoryByClass.get(type);
+        for (Directory dir : _directories) {
+            if (type.isAssignableFrom(dir.getClass()))
+                return (T)dir;
+        }
+        return null;
     }
 
     /**
-     * Indicates whether a given directory type has been created in this metadata
-     * repository.  Directories are created by calling {@link Metadata#getOrCreateDirectory(Class)}.
+     * Indicates whether an instance of the given directory type exists in this Metadata instance.
      *
      * @param type the {@link Directory} type
-     * @return true if the {@link Directory} has been created
+     * @return <code>true</code> if a {@link Directory} of the specified type exists, otherwise <code>false</code>
      */
-    public boolean containsDirectory(Class<? extends Directory> type)
+    public boolean containsDirectoryOfType(Class<? extends Directory> type)
     {
-        return _directoryByClass.containsKey(type);
+        for (Directory dir : _directories) {
+            if (type.isAssignableFrom(dir.getClass()))
+                return true;
+        }
+        return false;
     }
 
     /**
@@ -136,7 +127,7 @@ public final class Metadata
      */
     public boolean hasErrors()
     {
-        for (Directory directory : _directoryList) {
+        for (Directory directory : getDirectories()) {
             if (directory.hasErrors())
                 return true;
         }
@@ -146,9 +137,10 @@ public final class Metadata
     @Override
     public String toString()
     {
+        int count = getDirectoryCount();
         return String.format("Metadata (%d %s)",
-            _directoryList.size(),
-            _directoryList.size() == 1
+            count,
+            count == 1
                 ? "directory"
                 : "directories");
     }
diff --git a/Source/com/drew/metadata/MetadataException.java b/Source/com/drew/metadata/MetadataException.java
index 72109de..25ba8bb 100644
--- a/Source/com/drew/metadata/MetadataException.java
+++ b/Source/com/drew/metadata/MetadataException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/metadata/MetadataReader.java b/Source/com/drew/metadata/MetadataReader.java
index 5201149..9459fd3 100644
--- a/Source/com/drew/metadata/MetadataReader.java
+++ b/Source/com/drew/metadata/MetadataReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -38,5 +38,5 @@ public interface MetadataReader
      * @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);
+    void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata);
 }
diff --git a/Source/com/drew/metadata/Schema.java b/Source/com/drew/metadata/Schema.java
new file mode 100644
index 0000000..13cfa91
--- /dev/null
+++ b/Source/com/drew/metadata/Schema.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+
+public class Schema
+{
+    /**
+     * XMP tag namespace. TODO the older "xap", "xapBJ", "xapMM" or "xapRights" namespace prefixes should be translated to the newer "xmp", "xmpBJ",
+     * "xmpMM" and "xmpRights" prefixes for use in family 1 group names
+     */
+    @NotNull
+    public static final String XMP_PROPERTIES = "http://ns.adobe.com/xap/1.0/";
+    @NotNull
+    public static final String EXIF_SPECIFIC_PROPERTIES = "http://ns.adobe.com/exif/1.0/";
+    @NotNull
+    public static final String EXIF_ADDITIONAL_PROPERTIES = "http://ns.adobe.com/exif/1.0/aux/";
+    @NotNull
+    public static final String EXIF_TIFF_PROPERTIES = "http://ns.adobe.com/tiff/1.0/";
+    @NotNull
+    public static final String DUBLIN_CORE_SPECIFIC_PROPERTIES = "http://purl.org/dc/elements/1.1/";
+}
diff --git a/Source/com/drew/metadata/StringValue.java b/Source/com/drew/metadata/StringValue.java
new file mode 100644
index 0000000..8c656e9
--- /dev/null
+++ b/Source/com/drew/metadata/StringValue.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public final class StringValue
+{
+    @NotNull
+    private final byte[] _bytes;
+
+    @Nullable
+    private final Charset _charset;
+
+    public StringValue(@NotNull byte[] bytes, @Nullable Charset charset)
+    {
+        _bytes = bytes;
+        _charset = charset;
+    }
+
+    @NotNull
+    public byte[] getBytes()
+    {
+        return _bytes;
+    }
+
+    @Nullable
+    public Charset getCharset()
+    {
+        return _charset;
+    }
+
+    @Override
+    public String toString()
+    {
+        return toString(_charset);
+    }
+
+    public String toString(@Nullable Charset charset)
+    {
+        if (charset != null) {
+            try {
+                return new String(_bytes, charset.name());
+            } catch (UnsupportedEncodingException ex) {
+                // fall through
+            }
+        }
+
+        return new String(_bytes);
+    }
+}
diff --git a/Source/com/drew/metadata/Tag.java b/Source/com/drew/metadata/Tag.java
index f8603ef..3a05523 100644
--- a/Source/com/drew/metadata/Tag.java
+++ b/Source/com/drew/metadata/Tag.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import com.drew.lang.annotations.Nullable;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("unused")
 public class Tag
 {
     private final int _tagType;
@@ -53,16 +54,14 @@ public class Tag
 
     /**
      * Gets the tag type in hex notation as a String with padded leading
-     * zeroes if necessary (i.e. <code>0x100E</code>).
+     * zeroes if necessary (i.e. <code>0x100e</code>).
      *
      * @return the tag type as a string in hexadecimal notation
      */
     @NotNull
     public String getTagTypeHex()
     {
-        String hex = Integer.toHexString(_tagType);
-        while (hex.length() < 4) hex = "0" + hex;
-        return "0x" + hex;
+        return String.format("0x%04x", _tagType);
     }
 
     /**
@@ -85,7 +84,6 @@ public class Tag
      *
      * @return whether this tag has a name
      */
-    @NotNull
     public boolean hasTagName()
     {
         return _directory.hasTagName(_tagType);
@@ -116,7 +114,7 @@ public class Tag
     }
 
     /**
-     * A basic representation of the tag's type and value.  EG: <code>[FNumber] F2.8</code>.
+     * A basic representation of the tag's type and value.  EG: <code>[Exif IFD0] FNumber - f/2.8</code>.
      *
      * @return the tag's type and value
      */
diff --git a/Source/com/drew/metadata/TagDescriptor.java b/Source/com/drew/metadata/TagDescriptor.java
index 50e7e46..aef6bad 100644
--- a/Source/com/drew/metadata/TagDescriptor.java
+++ b/Source/com/drew/metadata/TagDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -27,6 +27,10 @@ import com.drew.lang.annotations.Nullable;
 
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
+import java.math.RoundingMode;
+import java.nio.charset.Charset;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -70,11 +74,18 @@ public class TagDescriptor<T extends Directory>
         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");
+                return String.format("[%d values]", length);
             }
         }
 
+        if (object instanceof Date)
+        {
+            // Produce a date string having a format that includes the offset in form "+00:00"
+            return new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy")
+                .format((Date) object)
+                .replaceAll("([0-9]{2} [^ ]+)$", ":$1");
+        }
+
         // no special handling required, so use default conversion to a string
         return _directory.getString(tagType);
     }
@@ -258,7 +269,7 @@ public class TagDescriptor<T extends Directory>
     }
 
     @Nullable
-    protected String getAsciiStringFromBytes(int tag)
+    protected String getStringFromBytes(int tag, Charset cs)
     {
         byte[] values = _directory.getByteArray(tag);
 
@@ -266,9 +277,190 @@ public class TagDescriptor<T extends Directory>
             return null;
 
         try {
-            return new String(values, "ASCII").trim();
+            return new String(values, cs.name()).trim();
         } catch (UnsupportedEncodingException e) {
             return null;
         }
     }
+
+    @Nullable
+    protected String getRationalOrDoubleString(int tagType)
+    {
+        Rational rational = _directory.getRational(tagType);
+        if (rational != null)
+            return rational.toSimpleString(true);
+
+        Double d = _directory.getDoubleObject(tagType);
+        if (d != null)
+        {
+            DecimalFormat format = new DecimalFormat("0.###");
+            return format.format(d);
+        }
+
+        return null;
+    }
+
+    @Nullable
+    protected static String getFStopDescription(double fStop)
+    {
+        DecimalFormat format = new DecimalFormat("0.0");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return "f/" + format.format(fStop);
+    }
+
+    @Nullable
+    protected static String getFocalLengthDescription(double mm)
+    {
+        DecimalFormat format = new DecimalFormat("0.#");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return format.format(mm) + " mm";
+    }
+
+    @Nullable
+    protected String getLensSpecificationDescription(int tag)
+    {
+        Rational[] values = _directory.getRationalArray(tag);
+
+        if (values == null || values.length != 4 || (values[0].isZero() && values[2].isZero()))
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        if (values[0].equals(values[1]))
+            sb.append(values[0].toSimpleString(true)).append("mm");
+        else
+            sb.append(values[0].toSimpleString(true)).append('-').append(values[1].toSimpleString(true)).append("mm");
+
+        if (!values[2].isZero()) {
+            sb.append(' ');
+
+            DecimalFormat format = new DecimalFormat("0.0");
+            format.setRoundingMode(RoundingMode.HALF_UP);
+
+            if (values[2].equals(values[3]))
+                sb.append(getFStopDescription(values[2].doubleValue()));
+            else
+                sb.append("f/").append(format.format(values[2].doubleValue())).append('-').append(format.format(values[3].doubleValue()));
+        }
+
+        return sb.toString();
+    }
+
+    @Nullable
+    protected String getOrientationDescription(int tag)
+    {
+        return getIndexedDescription(tag, 1,
+            "Top, left side (Horizontal / normal)",
+            "Top, right side (Mirror horizontal)",
+            "Bottom, right side (Rotate 180)",
+            "Bottom, left side (Mirror vertical)",
+            "Left side, top (Mirror horizontal and rotate 270 CW)",
+            "Right side, top (Rotate 90 CW)",
+            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
+            "Left side, bottom (Rotate 270 CW)");
+    }
+
+    @Nullable
+    protected String getShutterSpeedDescription(int tag)
+    {
+        // I believe this method to now be stable, but am leaving some alternative snippets of
+        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
+
+//        float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+//        int apexPower = (int)Math.pow(2.0, apexValue);
+//        return "1/" + apexPower + " sec";
+        // TODO test this method
+        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
+        // description (spotted bug using a Canon EOS 300D)
+        // thanks also to Gli Blr for spotting this bug
+        Float apexValue = _directory.getFloatObject(tag);
+        if (apexValue == null)
+            return null;
+        if (apexValue <= 1) {
+            float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
+            long apexPower10 = Math.round((double)apexPower * 10.0);
+            float fApexPower = (float)apexPower10 / 10.0f;
+            DecimalFormat format = new DecimalFormat("0.##");
+            format.setRoundingMode(RoundingMode.HALF_UP);
+            return format.format(fApexPower) + " sec";
+        } else {
+            int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
+            return "1/" + apexPower + " sec";
+        }
+
+/*
+        // This alternative implementation offered by Bill Richards
+        // TODO determine which is the correct / more-correct implementation
+        double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+        double apexPower = Math.pow(2.0, apexValue);
+
+        StringBuffer sb = new StringBuffer();
+        if (apexPower > 1)
+            apexPower = Math.floor(apexPower);
+
+        if (apexPower < 1) {
+            sb.append((int)Math.round(1/apexPower));
+        } else {
+            sb.append("1/");
+            sb.append((int)apexPower);
+        }
+        sb.append(" sec");
+        return sb.toString();
+*/
+    }
+
+    // EXIF LightSource
+    @Nullable
+    protected String getLightSourceDescription(short wbtype)
+    {
+        switch (wbtype)
+        {
+            case 0:
+                return "Unknown";
+            case 1:
+                return "Daylight";
+            case 2:
+                return "Fluorescent";
+            case 3:
+                return "Tungsten (Incandescent)";
+            case 4:
+                return "Flash";
+            case 9:
+                return "Fine Weather";
+            case 10:
+                return "Cloudy";
+            case 11:
+                return "Shade";
+            case 12:
+                return "Daylight Fluorescent";    // (D 5700 - 7100K)
+            case 13:
+                return "Day White Fluorescent";   // (N 4600 - 5500K)
+            case 14:
+                return "Cool White Fluorescent";  // (W 3800 - 4500K)
+            case 15:
+                return "White Fluorescent";       // (WW 3250 - 3800K)
+            case 16:
+                return "Warm White Fluorescent";  // (L 2600 - 3250K)
+            case 17:
+                return "Standard Light A";
+            case 18:
+                return "Standard Light B";
+            case 19:
+                return "Standard Light C";
+            case 20:
+                return "D55";
+            case 21:
+                return "D65";
+            case 22:
+                return "D75";
+            case 23:
+                return "D50";
+            case 24:
+                return "ISO Studio Tungsten";
+            case 255:
+                return "Other";
+        }
+
+        return getDescription(wbtype);
+    }
 }
diff --git a/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java b/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java
index 7d2e61b..26d74fb 100644
--- a/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java
+++ b/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,9 +24,12 @@ package com.drew.metadata.adobe;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.adobe.AdobeJpegDirectory.*;
+
 /**
  * Provides human-readable string versions of the tags stored in an AdobeJpegDirectory.
  */
+@SuppressWarnings("WeakerAccess")
 public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory>
 {
     public AdobeJpegDescriptor(AdobeJpegDirectory directory)
@@ -38,9 +41,9 @@ public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case AdobeJpegDirectory.TAG_COLOR_TRANSFORM:
+            case TAG_COLOR_TRANSFORM:
                 return getColorTransformDescription();
-            case AdobeJpegDirectory.TAG_DCT_ENCODE_VERSION:
+            case TAG_DCT_ENCODE_VERSION:
                 return getDctEncodeVersionDescription();
             default:
                 return super.getDescription(tagType);
@@ -50,7 +53,7 @@ public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory>
     @Nullable
     private String getDctEncodeVersionDescription()
     {
-        Integer value = _directory.getInteger(AdobeJpegDirectory.TAG_COLOR_TRANSFORM);
+        Integer value = _directory.getInteger(TAG_DCT_ENCODE_VERSION);
         return value == null
                 ? null
                 : value == 0x64
@@ -61,14 +64,9 @@ public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory>
     @Nullable
     private String getColorTransformDescription()
     {
-        Integer value = _directory.getInteger(AdobeJpegDirectory.TAG_COLOR_TRANSFORM);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 0: return "Unknown (RGB or CMYK)";
-            case 1: return "YCbCr";
-            case 2: return "YCCK";
-            default: return String.format("Unknown transform (%d)", value);
-        }
+        return getIndexedDescription(TAG_COLOR_TRANSFORM,
+            "Unknown (RGB or CMYK)",
+            "YCbCr",
+            "YCCK");
     }
 }
diff --git a/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java b/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java
index 8c5492c..f775249 100644
--- a/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java
+++ b/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import java.util.HashMap;
 /**
  * Contains image encoding information for DCT filters, as stored by Adobe.
  */
+@SuppressWarnings("WeakerAccess")
 public class AdobeJpegDirectory extends Directory {
 
     public static final int TAG_DCT_ENCODE_VERSION = 0;
diff --git a/Source/com/drew/metadata/adobe/AdobeJpegReader.java b/Source/com/drew/metadata/adobe/AdobeJpegReader.java
index be40745..d5d9101 100644
--- a/Source/com/drew/metadata/adobe/AdobeJpegReader.java
+++ b/Source/com/drew/metadata/adobe/AdobeJpegReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,38 +30,42 @@ import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * Decodes Adobe formatted data stored in JPEG files, normally in the APPE (App14) segment.
  *
- * @author Philip, Drew Noakes https://drewnoakes.com
+ * @author Philip
+ * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class AdobeJpegReader implements JpegSegmentMetadataReader
 {
+    public static final String PREAMBLE = "Adobe";
+
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APPE);
-    }
-
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
-    {
-        return segmentBytes.length == 12 && "Adobe".equalsIgnoreCase(new String(segmentBytes, 0, 5));
+        return Collections.singletonList(JpegSegmentType.APPE);
     }
 
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        extract(new SequentialByteArrayReader(segmentBytes), metadata);
+        for (byte[] bytes : segments) {
+            if (bytes.length == 12 && PREAMBLE.equalsIgnoreCase(new String(bytes, 0, PREAMBLE.length())))
+                extract(new SequentialByteArrayReader(bytes), metadata);
+        }
     }
 
     public void extract(@NotNull SequentialReader reader, @NotNull Metadata metadata)
     {
-        final Directory directory = metadata.getOrCreateDirectory(AdobeJpegDirectory.class);
+        Directory directory = new AdobeJpegDirectory();
+        metadata.addDirectory(directory);
+
         try {
             reader.setMotorolaByteOrder(false);
 
-            if (!reader.getString(5).equals("Adobe")) {
+            if (!reader.getString(PREAMBLE.length()).equals(PREAMBLE)) {
                 directory.addError("Invalid Adobe JPEG data header.");
                 return;
             }
diff --git a/Source/com/drew/metadata/adobe/package-info.java b/Source/com/drew/metadata/adobe/package-info.java
new file mode 100644
index 0000000..ea34838
--- /dev/null
+++ b/Source/com/drew/metadata/adobe/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of Adobe metadata.
+ */
+package com.drew.metadata.adobe;
diff --git a/Source/com/drew/metadata/adobe/package.html b/Source/com/drew/metadata/adobe/package.html
deleted file mode 100644
index 175c98b..0000000
--- a/Source/com/drew/metadata/adobe/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of Adobe metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java b/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java
index 4ea0ee1..b01ce7d 100644
--- a/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java
+++ b/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java
@@ -1,12 +1,35 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.bmp;
 
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.bmp.BmpHeaderDirectory.*;
+
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory>
 {
     public BmpHeaderDescriptor(@NotNull BmpHeaderDirectory directory)
@@ -18,7 +41,7 @@ public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case BmpHeaderDirectory.TAG_COMPRESSION:
+            case TAG_COMPRESSION:
                 return getCompressionDescription();
             default:
                 return super.getDescription(tagType);
@@ -36,10 +59,10 @@ public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory>
         // 5 = PNG
         // 6 = Bit field
         try {
-            Integer value = _directory.getInteger(BmpHeaderDirectory.TAG_COMPRESSION);
+            Integer value = _directory.getInteger(TAG_COMPRESSION);
             if (value == null)
                 return null;
-            Integer headerSize = _directory.getInteger(BmpHeaderDirectory.TAG_HEADER_SIZE);
+            Integer headerSize = _directory.getInteger(TAG_HEADER_SIZE);
             if (headerSize == null)
                 return null;
 
@@ -52,7 +75,7 @@ public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory>
                 case 5: return "PNG";
                 case 6: return "Bit field";
                 default:
-                    return super.getDescription(BmpHeaderDirectory.TAG_COMPRESSION);
+                    return super.getDescription(TAG_COMPRESSION);
             }
         } catch (Exception e) {
             return null;
diff --git a/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java b/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java
index 7124d51..e85c5c1 100644
--- a/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java
+++ b/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.bmp;
 
 import com.drew.lang.annotations.NotNull;
@@ -8,6 +28,7 @@ import java.util.HashMap;
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class BmpHeaderDirectory extends Directory
 {
     public static final int TAG_HEADER_SIZE = -1;
diff --git a/Source/com/drew/metadata/bmp/BmpReader.java b/Source/com/drew/metadata/bmp/BmpReader.java
index a3f4f20..798b5fa 100644
--- a/Source/com/drew/metadata/bmp/BmpReader.java
+++ b/Source/com/drew/metadata/bmp/BmpReader.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.bmp;
 
 import com.drew.lang.SequentialReader;
@@ -13,7 +33,8 @@ public class BmpReader
 {
     public void extract(@NotNull final SequentialReader reader, final @NotNull Metadata metadata)
     {
-        final BmpHeaderDirectory directory = metadata.getOrCreateDirectory(BmpHeaderDirectory.class);
+        BmpHeaderDirectory directory = new BmpHeaderDirectory();
+        metadata.addDirectory(directory);
 
         // FILE HEADER
         //
diff --git a/Source/com/drew/metadata/bmp/package-info.java b/Source/com/drew/metadata/bmp/package-info.java
new file mode 100644
index 0000000..ce10653
--- /dev/null
+++ b/Source/com/drew/metadata/bmp/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of BMP file metadata.
+ *
+ * @since 2.7.0
+ */
+package com.drew.metadata.bmp;
diff --git a/Source/com/drew/metadata/bmp/package.html b/Source/com/drew/metadata/bmp/package.html
deleted file mode 100644
index 15dbdc5..0000000
--- a/Source/com/drew/metadata/bmp/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of BMP file metadata.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/exif/ExifDescriptorBase.java b/Source/com/drew/metadata/exif/ExifDescriptorBase.java
new file mode 100644
index 0000000..551d0f1
--- /dev/null
+++ b/Source/com/drew/metadata/exif/ExifDescriptorBase.java
@@ -0,0 +1,1234 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.imaging.PhotographicConversions;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.lang.ByteArrayReader;
+import com.drew.metadata.Directory;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.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
+ */
+@SuppressWarnings("WeakerAccess")
+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;
+
+    // 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_CFA_PATTERN_2:
+                return getCfaPattern2Description();
+            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_CFA_PATTERN:
+                return getCfaPatternDescription();
+            case TAG_COMPONENTS_CONFIGURATION:
+                return getComponentConfigurationDescription();
+            case TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case TAG_FLASHPIX_VERSION:
+                return getFlashPixVersionDescription();
+            case TAG_ISO_EQUIVALENT:
+                return getIsoEquivalentDescription();
+            case TAG_USER_COMMENT:
+                return getUserCommentDescription();
+            case TAG_CUSTOM_RENDERED:
+                return getCustomRenderedDescription();
+            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();
+            case TAG_LENS_SPECIFICATION:
+                return getLensSpecificationDescription();
+            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()
+    {
+        // For some reason, sometimes this is read as a long[] and
+        // getIntArray isn't able to deal with it
+        int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
+        if (ints==null || ints.length < 6)
+        {
+            Object o = _directory.getObject(TAG_REFERENCE_BLACK_WHITE);
+            if (o != null && (o instanceof long[]))
+            {
+                long[] longs = (long[])o;
+                if (longs.length < 6)
+                    return null;
+
+                ints = new int[longs.length];
+                for (int i = 0; i < longs.length; i++)
+                    ints[i] = (int)longs[i];
+            }
+            else
+                return null;
+        }
+
+        int blackR = ints[0];
+        int whiteR = ints[1];
+        int blackG = ints[2];
+        int whiteG = ints[3];
+        int blackB = ints[4];
+        int whiteB = ints[5];
+        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
+    }
+
+    @Nullable
+    public String getYResolutionDescription()
+    {
+        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getXResolutionDescription()
+    {
+        Rational value = _directory.getRational(TAG_X_RESOLUTION);
+        if (value == null)
+            return null;
+        final String unit = getResolutionDescription();
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getYCbCrPositioningDescription()
+    {
+        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
+    }
+
+    @Nullable
+    public String getOrientationDescription()
+    {
+        return super.getOrientationDescription(TAG_ORIENTATION);
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
+    }
+
+    /** The Windows specific tags uses plain Unicode. */
+    @Nullable
+    private String getUnicodeDescription(int tag)
+    {
+        byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null)
+            return null;
+        try {
+            // Decode the unicode string and trim the unicode zero "\0" from the end.
+            return new String(bytes, "UTF-16LE").trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getWindowsAuthorDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_AUTHOR);
+    }
+
+    @Nullable
+    public String getWindowsCommentDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_COMMENT);
+    }
+
+    @Nullable
+    public String getWindowsKeywordsDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_KEYWORDS);
+    }
+
+    @Nullable
+    public String getWindowsTitleDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_TITLE);
+    }
+
+    @Nullable
+    public String getWindowsSubjectDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_SUBJECT);
+    }
+
+    @Nullable
+    public String getYCbCrSubsamplingDescription()
+    {
+        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
+        if (positions == null || positions.length < 2)
+            return null;
+        if (positions[0] == 2 && positions[1] == 1) {
+            return "YCbCr4:2:2";
+        } else if (positions[0] == 2 && positions[1] == 2) {
+            return "YCbCr4:2:0";
+        } else {
+            return "(Unknown)";
+        }
+    }
+
+    @Nullable
+    public String getPlanarConfigurationDescription()
+    {
+        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
+        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
+        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
+        // plane format.
+        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
+            1,
+            "Chunky (contiguous for each subsampling pixel)",
+            "Separate (Y-plane/Cb-plane/Cr-plane format)"
+        );
+    }
+
+    @Nullable
+    public String getSamplesPerPixelDescription()
+    {
+        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
+        return value == null ? null : value + " samples/pixel";
+    }
+
+    @Nullable
+    public String getRowsPerStripDescription()
+    {
+        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
+        return value == null ? null : value + " rows/strip";
+    }
+
+    @Nullable
+    public String getStripByteCountsDescription()
+    {
+        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
+        return value == null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getPhotometricInterpretationDescription()
+    {
+        // Shows the color space of the image data components
+        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "WhiteIsZero";
+            case 1: return "BlackIsZero";
+            case 2: return "RGB";
+            case 3: return "RGB Palette";
+            case 4: return "Transparency Mask";
+            case 5: return "CMYK";
+            case 6: return "YCbCr";
+            case 8: return "CIELab";
+            case 9: return "ICCLab";
+            case 10: return "ITULab";
+            case 32803: return "Color Filter Array";
+            case 32844: return "Pixar LogL";
+            case 32845: return "Pixar LogLuv";
+            case 32892: return "Linear Raw";
+            default:
+                return "Unknown colour space";
+        }
+    }
+
+    @Nullable
+    public String getBitsPerSampleDescription()
+    {
+        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
+        return value == null ? null : value + " bits/component/pixel";
+    }
+
+    @Nullable
+    public String getImageWidthDescription()
+    {
+        String value = _directory.getString(TAG_IMAGE_WIDTH);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getImageHeightDescription()
+    {
+        String value = _directory.getString(TAG_IMAGE_HEIGHT);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getNewSubfileTypeDescription()
+    {
+        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 0,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page image",
+            "Single page of multi-page reduced-resolution image",
+            "Transparency mask",
+            "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 getLensSpecificationDescription()
+    {
+        return getLensSpecificationDescription(TAG_LENS_SPECIFICATION);
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS,
+            "None",
+            "Low",
+            "Hard"
+        );
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION,
+            "None",
+            "Low saturation",
+            "High saturation"
+        );
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST,
+            "None",
+            "Soft",
+            "Hard"
+        );
+    }
+
+    @Nullable
+    public String getGainControlDescription()
+    {
+        return getIndexedDescription(TAG_GAIN_CONTROL,
+            "None",
+            "Low gain up",
+            "Low gain down",
+            "High gain up",
+            "High gain down"
+        );
+    }
+
+    @Nullable
+    public String getSceneCaptureTypeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE,
+            "Standard",
+            "Landscape",
+            "Portrait",
+            "Night scene"
+        );
+    }
+
+    @Nullable
+    public String get35mmFilmEquivFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
+        return value == null
+            ? null
+            : value == 0
+                ? "Unknown"
+                : getFocalLengthDescription(value);
+    }
+
+    @Nullable
+    public String getDigitalZoomRatioDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+                ? "Digital zoom not used"
+                : new DecimalFormat("0.#").format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getWhiteBalanceModeDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE_MODE,
+            "Auto white balance",
+            "Manual white balance"
+        );
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(TAG_EXPOSURE_MODE,
+            "Auto exposure",
+            "Manual exposure",
+            "Auto bracket"
+        );
+    }
+
+    @Nullable
+    public String getCustomRenderedDescription()
+    {
+        return getIndexedDescription(TAG_CUSTOM_RENDERED,
+            "Normal process",
+            "Custom process"
+        );
+    }
+
+    @Nullable
+    public String getUserCommentDescription()
+    {
+        byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT);
+        if (commentBytes == null)
+            return null;
+        if (commentBytes.length == 0)
+            return "";
+
+        final Map<String, String> encodingMap = new HashMap<String, String>();
+        encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
+        encodingMap.put("UNICODE", "UTF-16LE");
+        encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
+
+        try {
+            if (commentBytes.length >= 10) {
+                String firstTenBytesString = new String(commentBytes, 0, 10);
+
+                // try each encoding name
+                for (Map.Entry<String, String> pair : encodingMap.entrySet()) {
+                    String encodingName = pair.getKey();
+                    String charset = pair.getValue();
+                    if (firstTenBytesString.startsWith(encodingName)) {
+                        // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start
+                        for (int j = encodingName.length(); j < 10; j++) {
+                            byte b = commentBytes[j];
+                            if (b != '\0' && b != ' ')
+                                return new String(commentBytes, j, commentBytes.length - j, charset).trim();
+                        }
+                        return new String(commentBytes, 10, commentBytes.length - 10, charset).trim();
+                    }
+                }
+            }
+            // special handling fell through, return a plain string representation
+            return new String(commentBytes, System.getProperty("file.encoding")).trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getIsoEquivalentDescription()
+    {
+        // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values
+        Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT);
+        // There used to be a check here that multiplied ISO values < 50 by 200.
+        // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40.
+        return isoEquiv != null
+            ? Integer.toString(isoEquiv)
+            : null;
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
+    }
+
+    @Nullable
+    public String getFlashPixVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2);
+    }
+
+    @Nullable
+    public String getSceneTypeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_TYPE,
+            1,
+            "Directly photographed image"
+        );
+    }
+
+    /// <summary>
+    /// String description of CFA Pattern
+    /// </summary>
+    /// <remarks>
+    /// Converted from Exiftool version 10.33 created by Phil Harvey
+    /// http://www.sno.phy.queensu.ca/~phil/exiftool/
+    /// lib\Image\ExifTool\Exif.pm
+    ///
+    /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used.
+    /// It does not apply to all sensing methods.
+    /// </remarks>
+    @Nullable
+    public String getCfaPatternDescription()
+    {
+        return formatCFAPattern(decodeCfaPattern(TAG_CFA_PATTERN));
+    }
+
+    /// <summary>
+    /// String description of CFA Pattern
+    /// </summary>
+    /// <remarks>
+    /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used.
+    /// It does not apply to all sensing methods.
+    ///
+    /// ExifDirectoryBase.TAG_CFA_PATTERN_2 holds only the pixel pattern. ExifDirectoryBase.TAG_CFA_REPEAT_PATTERN_DIM is expected to exist and pass
+    /// some conditional tests.
+    /// </remarks>
+    @Nullable
+    public String getCfaPattern2Description()
+    {
+        byte[] values = _directory.getByteArray(TAG_CFA_PATTERN_2);
+        if (values == null)
+            return null;
+
+        int[] repeatPattern = _directory.getIntArray(TAG_CFA_REPEAT_PATTERN_DIM);
+        if (repeatPattern == null)
+            return String.format("Repeat Pattern not found for CFAPattern (%s)", super.getDescription(TAG_CFA_PATTERN_2));
+
+        if (repeatPattern.length == 2 && values.length == (repeatPattern[0] * repeatPattern[1]))
+        {
+            int[] intpattern = new int[2 + values.length];
+            intpattern[0] = repeatPattern[0];
+            intpattern[1] = repeatPattern[1];
+
+            for (int i = 0; i < values.length; i++)
+                intpattern[i + 2] = values[i] & 0xFF;   // convert the values[i] byte to unsigned
+
+            return formatCFAPattern(intpattern);
+        }
+
+        return String.format("Unknown Pattern (%s)", super.getDescription(TAG_CFA_PATTERN_2));
+    }
+
+    @Nullable
+    private static String formatCFAPattern(@Nullable int[] pattern)
+    {
+        if (pattern == null)
+            return null;
+        if (pattern.length < 2)
+            return "<truncated data>";
+        if (pattern[0] == 0 && pattern[1] == 0)
+            return "<zero pattern size>";
+
+        int end = 2 + pattern[0] * pattern[1];
+        if (end > pattern.length)
+            return "<invalid pattern size>";
+
+        String[] cfaColors = { "Red", "Green", "Blue", "Cyan", "Magenta", "Yellow", "White" };
+
+        StringBuilder ret = new StringBuilder();
+        ret.append("[");
+        for (int pos = 2; pos < end; pos++)
+        {
+            if (pattern[pos] <= cfaColors.length - 1)
+                ret.append(cfaColors[pattern[pos]]);
+            else
+                ret.append("Unknown");      // indicated pattern position is outside the array bounds
+
+            if ((pos - 2) % pattern[1] == 0)
+                ret.append(",");
+            else if(pos != end - 1)
+                ret.append("][");
+        }
+        ret.append("]");
+
+        return ret.toString();
+    }
+
+    /// <summary>
+    /// Decode raw CFAPattern value
+    /// </summary>
+    /// <remarks>
+    /// Converted from Exiftool version 10.33 created by Phil Harvey
+    /// http://www.sno.phy.queensu.ca/~phil/exiftool/
+    /// lib\Image\ExifTool\Exif.pm
+    ///
+    /// The value consists of:
+    /// - Two short, being the grid width and height of the repeated pattern.
+    /// - Next, for every pixel in that pattern, an identification code.
+    /// </remarks>
+    @Nullable
+    private int[] decodeCfaPattern(int tagType)
+    {
+        int[] ret;
+
+        byte[] values = _directory.getByteArray(tagType);
+        if (values == null)
+            return null;
+
+        if (values.length < 4)
+        {
+            ret = new int[values.length];
+            for (int i = 0; i < values.length; i++)
+                ret[i] = values[i];
+            return ret;
+        }
+
+        ret = new int[values.length - 2];
+
+        try {
+            ByteArrayReader reader = new ByteArrayReader(values);
+
+            // first two values should be read as 16-bits (2 bytes)
+            short item0 = reader.getInt16(0);
+            short item1 = reader.getInt16(2);
+
+            Boolean copyArray = false;
+            int end = 2 + item0 * item1;
+            if (end > values.length) // sanity check in case of byte order problems; calculated 'end' should be <= length of the values
+            {
+                // try swapping byte order (I have seen this order different than in EXIF)
+                reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder());
+                item0 = reader.getInt16(0);
+                item1 = reader.getInt16(2);
+
+                if (values.length >= (2 + item0 * item1))
+                    copyArray = true;
+            }
+            else
+                copyArray = true;
+
+            if(copyArray)
+            {
+                ret[0] = item0;
+                ret[1] = item1;
+
+                for (int i = 4; i < values.length; i++)
+                    ret[i - 2] = reader.getInt8(i);
+            }
+        } catch (IOException ex) {
+            _directory.addError("IO exception processing data: " + ex.getMessage());
+        }
+
+        return ret;
+    }
+
+    @Nullable
+    public String getFileSourceDescription()
+    {
+        return getIndexedDescription(TAG_FILE_SOURCE,
+            1,
+            "Film Scanner",
+            "Reflection Print Scanner",
+            "Digital Still Camera (DSC)"
+        );
+    }
+
+    @Nullable
+    public String getExposureBiasDescription()
+    {
+        Rational value = _directory.getRational(TAG_EXPOSURE_BIAS);
+        if (value == null)
+            return null;
+        return value.toSimpleString(true) + " EV";
+    }
+
+    @Nullable
+    public String getMaxApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return getFStopDescription(fStop);
+    }
+
+    @Nullable
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return getFStopDescription(fStop);
+    }
+
+    @Nullable
+    public String getExposureProgramDescription()
+    {
+        return getIndexedDescription(TAG_EXPOSURE_PROGRAM,
+            1,
+            "Manual control",
+            "Program normal",
+            "Aperture priority",
+            "Shutter priority",
+            "Program creative (slow program)",
+            "Program action (high-speed program)",
+            "Portrait mode",
+            "Landscape mode"
+        );
+    }
+
+
+    @Nullable
+    public String getFocalPlaneXResolutionDescription()
+    {
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION);
+        if (rational == null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit == null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneYResolutionDescription()
+    {
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION);
+        if (rational == null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit == null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneResolutionUnitDescription()
+    {
+        // Unit of FocalPlaneXResolution/FocalPlaneYResolution.
+        // '1' means no-unit, '2' inch, '3' centimeter.
+        return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+            1,
+            "(No unit)",
+            "Inches",
+            "cm"
+        );
+    }
+
+    @Nullable
+    public String getExifImageWidthDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH);
+        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);
+        return value == null ? null : getFocalLengthDescription(value.doubleValue());
+    }
+
+    @Nullable
+    public String getFlashDescription()
+    {
+        /*
+         * This is a bit mask.
+         * 0 = flash fired
+         * 1 = return detected
+         * 2 = return able to be detected
+         * 3 = unknown
+         * 4 = auto used
+         * 5 = unknown
+         * 6 = red eye reduction used
+         */
+
+        final Integer value = _directory.getInteger(TAG_FLASH);
+
+        if (value == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        if ((value & 0x1) != 0)
+            sb.append("Flash fired");
+        else
+            sb.append("Flash did not fire");
+
+        // check if we're able to detect a return, before we mention it
+        if ((value & 0x4) != 0) {
+            if ((value & 0x2) != 0)
+                sb.append(", return detected");
+            else
+                sb.append(", return not detected");
+        }
+
+        if ((value & 0x10) != 0)
+            sb.append(", auto");
+
+        if ((value & 0x40) != 0)
+            sb.append(", red-eye reduction");
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        // See http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
+        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Daylight";
+            case 2: return "Florescent";
+            case 3: return "Tungsten";
+            case 4: return "Flash";
+            case 9: return "Fine Weather";
+            case 10: return "Cloudy";
+            case 11: return "Shade";
+            case 12: return "Daylight Fluorescent";
+            case 13: return "Day White Fluorescent";
+            case 14: return "Cool White Fluorescent";
+            case 15: return "White Fluorescent";
+            case 16: return "Warm White Fluorescent";
+            case 17: return "Standard light";
+            case 18: return "Standard light (B)";
+            case 19: return "Standard light (C)";
+            case 20: return "D55";
+            case 21: return "D65";
+            case 22: return "D75";
+            case 23: return "D50";
+            case 24: return "Studio Tungsten";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
+        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
+        Integer value = _directory.getInteger(TAG_METERING_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Average";
+            case 2: return "Center weighted average";
+            case 3: return "Spot";
+            case 4: return "Multi-spot";
+            case 5: return "Multi-segment";
+            case 6: return "Partial";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @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;
+        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()
+    {
+        return super.getShutterSpeedDescription(TAG_SHUTTER_SPEED);
+    }
+
+    @Nullable
+    public String getFNumberDescription()
+    {
+        Rational value = _directory.getRational(TAG_FNUMBER);
+        if (value == null)
+            return null;
+        return getFStopDescription(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/Source/com/drew/metadata/exif/ExifDirectoryBase.java b/Source/com/drew/metadata/exif/ExifDirectoryBase.java
new file mode 100644
index 0000000..56c288d
--- /dev/null
+++ b/Source/com/drew/metadata/exif/ExifDirectoryBase.java
@@ -0,0 +1,764 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Base class for several Exif format tag directories.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+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_PAGE_NUMBER                       = 0x0129;
+
+    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;
+
+    /**
+     * Tag is a pointer to one or more sub-IFDs.
+     + Seems to be used exclusively by raw formats, referencing one or two IFDs.
+     */
+    public static final int TAG_SUB_IFD_OFFSET                    = 0x014a;
+
+    public static final int TAG_TRANSFER_RANGE                    = 0x0156;
+    public static final int TAG_JPEG_TABLES                       = 0x015B;
+    public static final int TAG_JPEG_PROC                         = 0x0200;
+
+    // 0x0201 can have all kinds of descriptions for thumbnail starting index
+    // 0x0202 can have all kinds of descriptions for thumbnail length
+    public static final int TAG_JPEG_RESTART_INTERVAL = 0x0203;
+    public static final int TAG_JPEG_LOSSLESS_PREDICTORS = 0x0205;
+    public static final int TAG_JPEG_POINT_TRANSFORMS = 0x0206;
+    public static final int TAG_JPEG_Q_TABLES = 0x0207;
+    public static final int TAG_JPEG_DC_TABLES = 0x0208;
+    public static final int TAG_JPEG_AC_TABLES = 0x0209;
+
+    public static final int TAG_YCBCR_COEFFICIENTS                = 0x0211;
+    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_STRIP_ROW_COUNTS                  = 0x022f;
+    public static final int TAG_APPLICATION_NOTES                 = 0x02bc;
+
+    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;
+
+    /**
+     * @deprecated use {@link com.drew.metadata.exif.ExifDirectoryBase#TAG_WHITE_BALANCE} instead.
+     */
+    @Deprecated
+    public static final int TAG_LIGHT_SOURCE                      = 0x9208;
+    /**
+     * White balance (aka light source). '0' means unknown, '1' daylight,
+     * '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;
+    /**
+     * 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_IMAGE_MATCHING_INFO         = 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_PAGE_NUMBER, "Page Number");
+        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_JPEG_RESTART_INTERVAL, "JPEG Restart Interval");
+        map.put(TAG_JPEG_LOSSLESS_PREDICTORS, "JPEG Lossless Predictors");
+        map.put(TAG_JPEG_POINT_TRANSFORMS, "JPEG Point Transforms");
+        map.put(TAG_JPEG_Q_TABLES, "JPEGQ Tables");
+        map.put(TAG_JPEG_DC_TABLES, "JPEGDC Tables");
+        map.put(TAG_JPEG_AC_TABLES, "JPEGAC Tables");
+
+        map.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
+        map.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
+        map.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
+        map.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+        map.put(TAG_STRIP_ROW_COUNTS, "Strip Row Counts");
+        map.put(TAG_APPLICATION_NOTES, "Application Notes");
+        map.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format");
+        map.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width");
+        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_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_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        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/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java b/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java
index bf2f3a8..6d8084b 100644
--- a/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java
+++ b/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,190 +21,18 @@
 
 package com.drew.metadata.exif;
 
-import com.drew.lang.Rational;
 import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.TagDescriptor;
-
-import java.io.UnsupportedEncodingException;
-
-import static com.drew.metadata.exif.ExifIFD0Directory.*;
 
 /**
  * Provides human-readable string representations of tag values stored in a {@link ExifIFD0Directory}.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory>
+@SuppressWarnings("WeakerAccess")
+public class ExifIFD0Descriptor extends ExifDescriptorBase<ExifIFD0Directory>
 {
-    /**
-     * 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;
-
     public ExifIFD0Descriptor(@NotNull ExifIFD0Directory directory)
     {
         super(directory);
     }
-
-    // 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.
-
-    /**
-     * Returns a descriptive value of the specified tag for this image.
-     * Where possible, known values will be substituted here in place of the raw
-     * tokens actually kept in the Exif segment.  If no substitution is
-     * available, the value provided by getString(int) will be returned.
-     * @param tagType the tag to find a description for
-     * @return a description of the image's value for the specified tag, or
-     *         <code>null</code> if the tag hasn't been defined.
-     */
-    @Override
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case 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_REFERENCE_BLACK_WHITE:
-                return getReferenceBlackWhiteDescription();
-            case TAG_ORIENTATION:
-                return getOrientationDescription();
-
-            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();
-
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    @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);
-    }
 }
diff --git a/Source/com/drew/metadata/exif/ExifIFD0Directory.java b/Source/com/drew/metadata/exif/ExifIFD0Directory.java
index e9cfb5c..1b393ba 100644
--- a/Source/com/drew/metadata/exif/ExifIFD0Directory.java
+++ b/Source/com/drew/metadata/exif/ExifIFD0Directory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,7 +22,6 @@
 package com.drew.metadata.exif;
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
 
 import java.util.HashMap;
 
@@ -31,83 +30,26 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifIFD0Directory extends Directory
+@SuppressWarnings("WeakerAccess")
+public class ExifIFD0Directory extends ExifDirectoryBase
 {
-    public static final int TAG_IMAGE_DESCRIPTION = 0x010E;
-    public static final int TAG_MAKE = 0x010F;
-    public static final int TAG_MODEL = 0x0110;
-    public static final int TAG_ORIENTATION = 0x0112;
-    public static final int TAG_X_RESOLUTION = 0x011A;
-    public static final int TAG_Y_RESOLUTION = 0x011B;
-    public static final int TAG_RESOLUTION_UNIT = 0x0128;
-    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_WHITE_POINT = 0x013E;
-    public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F;
-
-    public static final int TAG_YCBCR_COEFFICIENTS = 0x0211;
-    public static final int TAG_YCBCR_POSITIONING = 0x0213;
-    public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214;
-
-
     /** This tag is a pointer to the Exif SubIFD. */
     public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769;
 
     /** This tag is a pointer to the Exif GPS IFD. */
     public static final int TAG_GPS_INFO_OFFSET = 0x8825;
 
-    public static final int TAG_COPYRIGHT = 0x8298;
-
-    /** Non-standard, but in use. */
-    public static final int TAG_TIME_ZONE_OFFSET = 0x882a;
-
-    /** The image title, as used by Windows XP. */
-    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 ExifIFD0Directory()
+    {
+        this.setDescriptor(new ExifIFD0Descriptor(this));
+    }
 
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        _tagNameMap.put(TAG_IMAGE_DESCRIPTION, "Image Description");
-        _tagNameMap.put(TAG_MAKE, "Make");
-        _tagNameMap.put(TAG_MODEL, "Model");
-        _tagNameMap.put(TAG_ORIENTATION, "Orientation");
-        _tagNameMap.put(TAG_X_RESOLUTION, "X Resolution");
-        _tagNameMap.put(TAG_Y_RESOLUTION, "Y Resolution");
-        _tagNameMap.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
-        _tagNameMap.put(TAG_SOFTWARE, "Software");
-        _tagNameMap.put(TAG_DATETIME, "Date/Time");
-        _tagNameMap.put(TAG_ARTIST, "Artist");
-        _tagNameMap.put(TAG_WHITE_POINT, "White Point");
-        _tagNameMap.put(TAG_PRIMARY_CHROMATICITIES, "Primary Chromaticities");
-        _tagNameMap.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
-        _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
-        _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
-
-        _tagNameMap.put(TAG_COPYRIGHT, "Copyright");
-
-        _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
-
-        _tagNameMap.put(TAG_WIN_AUTHOR, "Windows XP Author");
-        _tagNameMap.put(TAG_WIN_COMMENT, "Windows XP Comment");
-        _tagNameMap.put(TAG_WIN_KEYWORDS, "Windows XP Keywords");
-        _tagNameMap.put(TAG_WIN_SUBJECT, "Windows XP Subject");
-        _tagNameMap.put(TAG_WIN_TITLE, "Windows XP Title");
-    }
-
-    public ExifIFD0Directory()
-    {
-        this.setDescriptor(new ExifIFD0Descriptor(this));
+        addExifTagNames(_tagNameMap);
     }
 
     @Override
diff --git a/Source/com/drew/metadata/exif/ExifImageDescriptor.java b/Source/com/drew/metadata/exif/ExifImageDescriptor.java
new file mode 100644
index 0000000..2e11159
--- /dev/null
+++ b/Source/com/drew/metadata/exif/ExifImageDescriptor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ExifImageDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ExifImageDescriptor extends ExifDescriptorBase<ExifImageDirectory>
+{
+    public ExifImageDescriptor(@NotNull ExifImageDirectory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/Source/com/drew/metadata/exif/ExifImageDirectory.java b/Source/com/drew/metadata/exif/ExifImageDirectory.java
new file mode 100644
index 0000000..8da34c1
--- /dev/null
+++ b/Source/com/drew/metadata/exif/ExifImageDirectory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.util.HashMap;
+
+/**
+ * Describes One of several Exif directories.
+ *
+ * Holds information about image IFD's in a chain after the first. The first page is stored in IFD0.
+ * Currently, this only applied to multi-page TIFF images
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ExifImageDirectory extends ExifDirectoryBase
+{
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        addExifTagNames(_tagNameMap);
+    }
+
+    public ExifImageDirectory()
+    {
+        this.setDescriptor(new ExifImageDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Exif Image";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/ExifInteropDescriptor.java b/Source/com/drew/metadata/exif/ExifInteropDescriptor.java
index 185ab11..2029f42 100644
--- a/Source/com/drew/metadata/exif/ExifInteropDescriptor.java
+++ b/Source/com/drew/metadata/exif/ExifInteropDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,53 +21,17 @@
 package com.drew.metadata.exif;
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.TagDescriptor;
-
-import static com.drew.metadata.exif.ExifInteropDirectory.*;
 
 /**
  * Provides human-readable string representations of tag values stored in a {@link ExifInteropDirectory}.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory>
+@SuppressWarnings("WeakerAccess")
+public class ExifInteropDescriptor extends ExifDescriptorBase<ExifInteropDirectory>
 {
     public ExifInteropDescriptor(@NotNull ExifInteropDirectory directory)
     {
         super(directory);
     }
-
-    @Override
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case TAG_INTEROP_INDEX:
-                return getInteropIndexDescription();
-            case TAG_INTEROP_VERSION:
-                return getInteropVersionDescription();
-            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 + ")";
-    }
 }
diff --git a/Source/com/drew/metadata/exif/ExifInteropDirectory.java b/Source/com/drew/metadata/exif/ExifInteropDirectory.java
index 7e6e68d..ee0f543 100644
--- a/Source/com/drew/metadata/exif/ExifInteropDirectory.java
+++ b/Source/com/drew/metadata/exif/ExifInteropDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@
 package com.drew.metadata.exif;
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
 
 import java.util.HashMap;
 
@@ -30,24 +29,15 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifInteropDirectory extends Directory
+@SuppressWarnings("WeakerAccess")
+public class ExifInteropDirectory extends ExifDirectoryBase
 {
-    public static final int TAG_INTEROP_INDEX = 0x0001;
-    public static final int TAG_INTEROP_VERSION = 0x0002;
-    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_LENGTH = 0x1002;
-
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        _tagNameMap.put(TAG_INTEROP_INDEX, "Interoperability Index");
-        _tagNameMap.put(TAG_INTEROP_VERSION, "Interoperability Version");
-        _tagNameMap.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format");
-        _tagNameMap.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width");
-        _tagNameMap.put(TAG_RELATED_IMAGE_LENGTH, "Related Image Length");
+        addExifTagNames(_tagNameMap);
     }
 
     public ExifInteropDirectory()
diff --git a/Source/com/drew/metadata/exif/ExifReader.java b/Source/com/drew/metadata/exif/ExifReader.java
index ecbc95a..ffc77c6 100644
--- a/Source/com/drew/metadata/exif/ExifReader.java
+++ b/Source/com/drew/metadata/exif/ExifReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -25,11 +25,14 @@ 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.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory},
@@ -38,77 +41,60 @@ import java.util.Arrays;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class ExifReader implements JpegSegmentMetadataReader
 {
-    /**
-     * The offset at which the TIFF data actually starts. This may be necessary when, for example, processing
-     * JPEG Exif data from APP0 which has a 6-byte preamble before starting the TIFF data.
-     */
-    private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
+    /** 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()
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return _storeThumbnailBytes;
+        return Collections.singletonList(JpegSegmentType.APP1);
     }
 
-    public void setStoreThumbnailBytes(boolean storeThumbnailBytes)
+    public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
     {
-        _storeThumbnailBytes = storeThumbnailBytes;
+        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());
+        }
     }
 
-    @NotNull
-    public Iterable<JpegSegmentType> getSegmentTypes()
+    /** Reads TIFF formatted Exif data from start of the specified {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
     {
-        return Arrays.asList(JpegSegmentType.APP1);
+        extract(reader, metadata, 0);
     }
 
-    public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType)
+    /** 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)
     {
-        return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE);
+        extract(reader, metadata, readerOffset, null);
     }
 
-    public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
+    /** Reads TIFF formatted Exif data at a specified offset within a {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset, @Nullable Directory parentDirectory)
     {
-        if (segmentBytes == null)
-            throw new NullPointerException("segmentBytes cannot be null");
-        if (metadata == null)
-            throw new NullPointerException("metadata cannot be null");
-        if (segmentType == null)
-            throw new NullPointerException("segmentType cannot be null");
+        ExifTiffHandler exifTiffHandler = new ExifTiffHandler(metadata, parentDirectory);
 
         try {
-            ByteArrayReader reader = new ByteArrayReader(segmentBytes);
-
-            //
-            // Check for the header preamble
-            //
-            try {
-                if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) {
-                    // TODO what do to with this error state?
-                    System.err.println("Invalid JPEG Exif segment preamble");
-                    return;
-                }
-            } catch (IOException e) {
-                // TODO what do to with this error state?
-                e.printStackTrace(System.err);
-                return;
-            }
-
-            //
             // Read the TIFF-formatted Exif data
-            //
             new TiffReader().processTiff(
                 reader,
-                new ExifTiffHandler(metadata, _storeThumbnailBytes),
-                JPEG_EXIF_SEGMENT_PREAMBLE.length()
+                exifTiffHandler,
+                readerOffset
             );
-
         } catch (TiffProcessingException e) {
+            exifTiffHandler.error("Exception processing TIFF data: " + e.getMessage());
             // TODO what do to with this error state?
             e.printStackTrace(System.err);
         } catch (IOException e) {
+            exifTiffHandler.error("Exception processing TIFF data: " + e.getMessage());
             // TODO what do to with this error state?
             e.printStackTrace(System.err);
         }
diff --git a/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java b/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java
index 46ca073..e00b171 100644
--- a/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java
+++ b/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -20,816 +20,18 @@
  */
 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.TagDescriptor;
-
-import java.io.UnsupportedEncodingException;
-import java.text.DecimalFormat;
-import java.util.HashMap;
-import java.util.Map;
-
-import static com.drew.metadata.exif.ExifSubIFDDirectory.*;
 
 /**
  * Provides human-readable string representations of tag values stored in a {@link ExifSubIFDDirectory}.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory>
+@SuppressWarnings("WeakerAccess")
+public class ExifSubIFDDescriptor extends ExifDescriptorBase<ExifSubIFDDirectory>
 {
-    /**
-     * 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.#");
-
     public ExifSubIFDDescriptor(@NotNull ExifSubIFDDirectory directory)
     {
         super(directory);
     }
-
-    // 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.
-
-    /**
-     * Returns a descriptive value of the specified tag for this image.
-     * Where possible, known values will be substituted here in place of the raw
-     * tokens actually kept in the Exif segment.  If no substitution is
-     * available, the value provided by getString(int) will be returned.
-     *
-     * @param tagType the tag to find a description for
-     * @return a description of the image's value for the specified tag, or
-     *         <code>null</code> if the tag hasn't been defined.
-     */
-    @Override
-    @Nullable
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case 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_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_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();
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    @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 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" + SimpleDecimalFormatter.format(fStop);
-    }
-
-    @Nullable
-    public String getApertureValueDescription()
-    {
-        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
-        if (aperture == null)
-            return null;
-        double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return "F" + SimpleDecimalFormatter.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 getYCbCrSubsamplingDescription()
-    {
-        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
-        if (positions == null)
-            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 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, '10' flash,
-        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
-        // '21' D65, '22' D75, '255' other.
-        final Integer value = _directory.getInteger(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 10: return "Flash";
-            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 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 "";
-        }
-    }
-
-    @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" + SimpleDecimalFormatter.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();
-    }
 }
diff --git a/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java b/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java
index 92b0944..a1c7319 100644
--- a/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java
+++ b/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,632 +21,34 @@
 package com.drew.metadata.exif;
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
+import com.drew.lang.annotations.Nullable;
 
+import java.util.Date;
 import java.util.HashMap;
+import java.util.TimeZone;
 
 /**
  * Describes Exif tags from the SubIFD directory.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifSubIFDDirectory extends Directory
+@SuppressWarnings("WeakerAccess")
+public class ExifSubIFDDirectory extends ExifDirectoryBase
 {
-    /**
-     * 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;
-    /**
-     * 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;
-
-    /**
-     * 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;
-
-    /** The position in the file of raster data. */
-    public static final int TAG_STRIP_OFFSETS = 0x0111;
-    /** 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 = 0x116;
-    /** 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;
-    /**
-     * 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_YCBCR_SUBSAMPLING = 0x0212;
-
-    /**
-     * 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_TRANSFER_FUNCTION = 0x012D;
-    public static final int TAG_PREDICTOR = 0x013D;
-    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_JPEG_TABLES = 0x015B;
-    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_IPTC_NAA = 0x83BB;
-    public static final int TAG_INTER_COLOR_PROFILE = 0x8773;
-    public static final int TAG_SPECTRAL_SENSITIVITY = 0x8824;
-    /**
-     * 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 = 0x882A;
-    public static final int TAG_SELF_TIMER_MODE = 0x882B;
-    public static final int TAG_FLASH_ENERGY = 0x920B;
-    public static final int TAG_SPATIAL_FREQ_RESPONSE = 0x920C;
-    public static final int TAG_NOISE = 0x920D;
-    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 = 0x9214;
-    /** There are two definitions for exposure index, I don't know the difference... */
-    public static final int TAG_EXPOSURE_INDEX_2 = 0x9215;
-    public static final int TAG_TIFF_EP_STANDARD_ID = 0x9216;
-    public static final int TAG_FLASH_ENERGY_2 = 0xA20B;
-    public static final int TAG_SPATIAL_FREQ_RESPONSE_2 = 0xA20C;
-    public static final int TAG_SUBJECT_LOCATION_2 = 0xA214;
-    public static final int TAG_PAGE_NAME = 0x011D;
-    /**
-     * 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;
-    /**
-     * 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_ISO_EQUIVALENT = 0x8827;
-    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;
-    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;
-    /**
-     * 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;
-    /**
-     * 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;
-
-    /**
-     * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs.
-     * To determine the format, we consider the starting bytes of the makernote itself and sometimes the
-     * camera model and make.
-     * <p>
-     * The component count for this tag includes all of the bytes needed for the makernote.
-     */
-    public static final int TAG_MAKERNOTE = 0x927C;
-
-    public static final int TAG_USER_COMMENT = 0x9286;
-
-    public static final int TAG_SUBSECOND_TIME = 0x9290;
-    public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291;
-    public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292;
-
-    public static final int TAG_FLASHPIX_VERSION = 0xA000;
-    /**
-     * 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;
-
     /** This tag is a pointer to the Exif Interop IFD. */
     public static final int TAG_INTEROP_OFFSET = 0xA005;
 
-    public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xA20E;
-    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xA20F;
-    /**
-     * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
-     * '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_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;
-
-    // these tags new with Exif 2.2 (?) [A401 - A4
-    /**
-     * 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;
-
-    // TODO support this tag (I haven't seen a camera's actual implementation of this yet)
-
-    /**
-     * 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_LENS = 0xFDEA;
+    public ExifSubIFDDirectory()
+    {
+        this.setDescriptor(new ExifSubIFDDescriptor(this));
+    }
 
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        _tagNameMap.put(TAG_FILL_ORDER, "Fill Order");
-        _tagNameMap.put(TAG_DOCUMENT_NAME, "Document Name");
-        // TODO why don't these tags have fields associated with them?
-        _tagNameMap.put(0x1000, "Related Image File Format");
-        _tagNameMap.put(0x1001, "Related Image Width");
-        _tagNameMap.put(0x1002, "Related Image Length");
-        _tagNameMap.put(0x0156, "Transfer Range");
-        _tagNameMap.put(0x0200, "JPEG Proc");
-        _tagNameMap.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel");
-        _tagNameMap.put(TAG_MAKERNOTE, "Makernote");
-        _tagNameMap.put(TAG_INTEROP_OFFSET, "Interoperability Offset");
-
-        _tagNameMap.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type");
-        _tagNameMap.put(TAG_SUBFILE_TYPE, "Subfile Type");
-        _tagNameMap.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample");
-        _tagNameMap.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation");
-        _tagNameMap.put(TAG_THRESHOLDING, "Thresholding");
-        _tagNameMap.put(TAG_STRIP_OFFSETS, "Strip Offsets");
-        _tagNameMap.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel");
-        _tagNameMap.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
-        _tagNameMap.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
-        _tagNameMap.put(TAG_PAGE_NAME, "Page Name");
-        _tagNameMap.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration");
-        _tagNameMap.put(TAG_TRANSFER_FUNCTION, "Transfer Function");
-        _tagNameMap.put(TAG_PREDICTOR, "Predictor");
-        _tagNameMap.put(TAG_TILE_WIDTH, "Tile Width");
-        _tagNameMap.put(TAG_TILE_LENGTH, "Tile Length");
-        _tagNameMap.put(TAG_TILE_OFFSETS, "Tile Offsets");
-        _tagNameMap.put(TAG_TILE_BYTE_COUNTS, "Tile Byte Counts");
-        _tagNameMap.put(TAG_JPEG_TABLES, "JPEG Tables");
-        _tagNameMap.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
-        _tagNameMap.put(TAG_CFA_REPEAT_PATTERN_DIM, "CFA Repeat Pattern Dim");
-        _tagNameMap.put(TAG_CFA_PATTERN_2, "CFA Pattern");
-        _tagNameMap.put(TAG_BATTERY_LEVEL, "Battery Level");
-        _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time");
-        _tagNameMap.put(TAG_FNUMBER, "F-Number");
-        _tagNameMap.put(TAG_IPTC_NAA, "IPTC/NAA");
-        _tagNameMap.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile");
-        _tagNameMap.put(TAG_EXPOSURE_PROGRAM, "Exposure Program");
-        _tagNameMap.put(TAG_SPECTRAL_SENSITIVITY, "Spectral Sensitivity");
-        _tagNameMap.put(TAG_ISO_EQUIVALENT, "ISO Speed Ratings");
-        _tagNameMap.put(TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, "Opto-electric Conversion Function (OECF)");
-        _tagNameMap.put(TAG_INTERLACE, "Interlace");
-        _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
-        _tagNameMap.put(TAG_SELF_TIMER_MODE, "Self Timer Mode");
-        _tagNameMap.put(TAG_EXIF_VERSION, "Exif Version");
-        _tagNameMap.put(TAG_DATETIME_ORIGINAL, "Date/Time Original");
-        _tagNameMap.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized");
-        _tagNameMap.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration");
-        _tagNameMap.put(TAG_SHUTTER_SPEED, "Shutter Speed Value");
-        _tagNameMap.put(TAG_APERTURE, "Aperture Value");
-        _tagNameMap.put(TAG_BRIGHTNESS_VALUE, "Brightness Value");
-        _tagNameMap.put(TAG_EXPOSURE_BIAS, "Exposure Bias Value");
-        _tagNameMap.put(TAG_MAX_APERTURE, "Max Aperture Value");
-        _tagNameMap.put(TAG_SUBJECT_DISTANCE, "Subject Distance");
-        _tagNameMap.put(TAG_METERING_MODE, "Metering Mode");
-        _tagNameMap.put(TAG_LIGHT_SOURCE, "Light Source");
-        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
-        _tagNameMap.put(TAG_FLASH, "Flash");
-        _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length");
-        _tagNameMap.put(TAG_FLASH_ENERGY, "Flash Energy");
-        _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE, "Spatial Frequency Response");
-        _tagNameMap.put(TAG_NOISE, "Noise");
-        _tagNameMap.put(TAG_IMAGE_NUMBER, "Image Number");
-        _tagNameMap.put(TAG_SECURITY_CLASSIFICATION, "Security Classification");
-        _tagNameMap.put(TAG_IMAGE_HISTORY, "Image History");
-        _tagNameMap.put(TAG_SUBJECT_LOCATION, "Subject Location");
-        _tagNameMap.put(TAG_EXPOSURE_INDEX, "Exposure Index");
-        _tagNameMap.put(TAG_TIFF_EP_STANDARD_ID, "TIFF/EP Standard ID");
-        _tagNameMap.put(TAG_USER_COMMENT, "User Comment");
-        _tagNameMap.put(TAG_SUBSECOND_TIME, "Sub-Sec Time");
-        _tagNameMap.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original");
-        _tagNameMap.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized");
-        _tagNameMap.put(TAG_FLASHPIX_VERSION, "FlashPix Version");
-        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
-        _tagNameMap.put(TAG_EXIF_IMAGE_WIDTH, "Exif Image Width");
-        _tagNameMap.put(TAG_EXIF_IMAGE_HEIGHT, "Exif Image Height");
-        _tagNameMap.put(TAG_RELATED_SOUND_FILE, "Related Sound File");
-        // 0x920B in TIFF/EP
-        _tagNameMap.put(TAG_FLASH_ENERGY_2, "Flash Energy");
-        // 0x920C in TIFF/EP
-        _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE_2, "Spatial Frequency Response");
-        // 0x920E in TIFF/EP
-        _tagNameMap.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution");
-        // 0x920F in TIFF/EP
-        _tagNameMap.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution");
-        // 0x9210 in TIFF/EP
-        _tagNameMap.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit");
-        // 0x9214 in TIFF/EP
-        _tagNameMap.put(TAG_SUBJECT_LOCATION_2, "Subject Location");
-        // 0x9215 in TIFF/EP
-        _tagNameMap.put(TAG_EXPOSURE_INDEX_2, "Exposure Index");
-        // 0x9217 in TIFF/EP
-        _tagNameMap.put(TAG_SENSING_METHOD, "Sensing Method");
-        _tagNameMap.put(TAG_FILE_SOURCE, "File Source");
-        _tagNameMap.put(TAG_SCENE_TYPE, "Scene Type");
-        _tagNameMap.put(TAG_CFA_PATTERN, "CFA Pattern");
-
-        _tagNameMap.put(TAG_CUSTOM_RENDERED, "Custom Rendered");
-        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
-        _tagNameMap.put(TAG_WHITE_BALANCE_MODE, "White Balance Mode");
-        _tagNameMap.put(TAG_DIGITAL_ZOOM_RATIO, "Digital Zoom Ratio");
-        _tagNameMap.put(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, "Focal Length 35");
-        _tagNameMap.put(TAG_SCENE_CAPTURE_TYPE, "Scene Capture Type");
-        _tagNameMap.put(TAG_GAIN_CONTROL, "Gain Control");
-        _tagNameMap.put(TAG_CONTRAST, "Contrast");
-        _tagNameMap.put(TAG_SATURATION, "Saturation");
-        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
-        _tagNameMap.put(TAG_DEVICE_SETTING_DESCRIPTION, "Device Setting Description");
-        _tagNameMap.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range");
-        _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID");
-
-        _tagNameMap.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name");
-        _tagNameMap.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number");
-        _tagNameMap.put(TAG_LENS_SPECIFICATION, "Lens Specification");
-        _tagNameMap.put(TAG_LENS_MAKE, "Lens Make");
-        _tagNameMap.put(TAG_LENS_MODEL, "Lens Model");
-        _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
-        _tagNameMap.put(TAG_GAMMA, "Gamma");
-
-        _tagNameMap.put(TAG_MIN_SAMPLE_VALUE, "Minimum sample value");
-        _tagNameMap.put(TAG_MAX_SAMPLE_VALUE, "Maximum sample value");
-
-        _tagNameMap.put(TAG_LENS, "Lens");
-    }
-
-    public ExifSubIFDDirectory()
-    {
-        this.setDescriptor(new ExifSubIFDDescriptor(this));
+        addExifTagNames(_tagNameMap);
     }
 
     @Override
@@ -662,4 +64,60 @@ public class ExifSubIFDDirectory extends Directory
     {
         return _tagNameMap;
     }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was captured.  Attempts will be made to parse the
+     * values as though it is in the GMT {@link TimeZone}.
+     *
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateOriginal()
+    {
+        return getDateOriginal(null);
+    }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was captured.  Attempts will be made to parse the
+     * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter
+     * (if it is non-null).
+     *
+     * @param timeZone the time zone to use
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateOriginal(@Nullable TimeZone timeZone)
+    {
+        return getDate(TAG_DATETIME_ORIGINAL, getString(TAG_SUBSECOND_TIME_ORIGINAL), timeZone);
+    }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was digitized.  Attempts will be made to parse the
+     * values as though it is in the GMT {@link TimeZone}.
+     *
+     * @return A Date object representing when this image was digitized, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateDigitized()
+    {
+        return getDateDigitized(null);
+    }
+
+    /**
+     * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds
+     * representing the date and time when this image was digitized.  Attempts will be made to parse the
+     * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter
+     * (if it is non-null).
+     *
+     * @param timeZone the time zone to use
+     * @return A Date object representing when this image was digitized, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateDigitized(@Nullable TimeZone timeZone)
+    {
+        return getDate(TAG_DATETIME_DIGITIZED, getString(TAG_SUBSECOND_TIME_DIGITIZED), timeZone);
+    }
 }
diff --git a/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java b/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java
index 0e36110..eb0014a 100644
--- a/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java
+++ b/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,10 +21,8 @@
 
 package com.drew.metadata.exif;
 
-import com.drew.lang.Rational;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.TagDescriptor;
 
 import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
 
@@ -33,233 +31,28 @@ import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirectory>
+@SuppressWarnings("WeakerAccess")
+public class ExifThumbnailDescriptor extends ExifDescriptorBase<ExifThumbnailDirectory>
 {
-    /**
-     * 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;
-
     public ExifThumbnailDescriptor(@NotNull ExifThumbnailDirectory directory)
     {
         super(directory);
     }
 
-    // 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.
-
-    /**
-     * Returns a descriptive value of the specified tag for this image.
-     * Where possible, known values will be substituted here in place of the raw
-     * tokens actually kept in the Exif segment.  If no substitution is
-     * available, the value provided by getString(int) will be returned.
-     *
-     * @param tagType the tag to find a description for
-     * @return a description of the image's value for the specified tag, or
-     *         <code>null</code> if the tag hasn't been defined.
-     */
     @Override
     @Nullable
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case 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_THUMBNAIL_OFFSET:
                 return getThumbnailOffsetDescription();
             case TAG_THUMBNAIL_LENGTH:
                 return getThumbnailLengthDescription();
-            case TAG_THUMBNAIL_IMAGE_WIDTH:
-                return getThumbnailImageWidthDescription();
-            case TAG_THUMBNAIL_IMAGE_HEIGHT:
-                return getThumbnailImageHeightDescription();
-            case TAG_BITS_PER_SAMPLE:
-                return getBitsPerSampleDescription();
-            case TAG_THUMBNAIL_COMPRESSION:
-                return getCompressionDescription();
-            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();
             default:
                 return super.getDescription(tagType);
         }
     }
 
-    @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 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 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 getBitsPerSampleDescription()
-    {
-        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
-        return value == null ? null : value + " bits/component/pixel";
-    }
-
-    @Nullable
-    public String getThumbnailImageWidthDescription()
-    {
-        String value = _directory.getString(TAG_THUMBNAIL_IMAGE_WIDTH);
-        return value == null ? null : value + " pixels";
-    }
-
-    @Nullable
-    public String getThumbnailImageHeightDescription()
-    {
-        String value = _directory.getString(TAG_THUMBNAIL_IMAGE_HEIGHT);
-        return value == null ? null : value + " pixels";
-    }
-
     @Nullable
     public String getThumbnailLengthDescription()
     {
@@ -273,55 +66,4 @@ public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirector
         String value = _directory.getString(TAG_THUMBNAIL_OFFSET);
         return value == null ? null : value + " bytes";
     }
-
-    @Nullable
-    public String getYResolutionDescription()
-    {
-        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
-        if (value == null)
-            return null;
-        final String unit = getResolutionDescription();
-        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
-            " dots per " +
-            (unit == null ? "unit" : unit.toLowerCase());
-    }
-
-    @Nullable
-    public String getXResolutionDescription()
-    {
-        Rational value = _directory.getRational(TAG_X_RESOLUTION);
-        if (value == null)
-            return null;
-        final String unit = getResolutionDescription();
-        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
-            " dots per " +
-            (unit == null ? "unit" : unit.toLowerCase());
-    }
-
-    @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");
-    }
 }
diff --git a/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java b/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java
index 86e9023..56122e3 100644
--- a/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java
+++ b/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,12 +22,7 @@
 package com.drew.metadata.exif;
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
 
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.util.HashMap;
 
 /**
@@ -35,95 +30,9 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifThumbnailDirectory extends Directory
+@SuppressWarnings("WeakerAccess")
+public class ExifThumbnailDirectory extends ExifDirectoryBase
 {
-    public static final int TAG_THUMBNAIL_IMAGE_WIDTH = 0x0100;
-    public static final int TAG_THUMBNAIL_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;
-
-    /**
-     * Shows compression method for Thumbnail.
-     * 1 = Uncompressed
-     * 2 = CCITT 1D
-     * 3 = T4/Group 3 Fax
-     * 4 = T6/Group 4 Fax
-     * 5 = LZW
-     * 6 = JPEG (old-style)
-     * 7 = JPEG
-     * 8 = Adobe Deflate
-     * 9 = JBIG B&amp;W
-     * 10 = JBIG Color
-     * 32766 = Next
-     * 32771 = CCIRLEW
-     * 32773 = PackBits
-     * 32809 = Thunderscan
-     * 32895 = IT8CTPAD
-     * 32896 = IT8LW
-     * 32897 = IT8MP
-     * 32898 = IT8BL
-     * 32908 = PixarFilm
-     * 32909 = PixarLog
-     * 32946 = Deflate
-     * 32947 = DCS
-     * 34661 = JBIG
-     * 34676 = SGILog
-     * 34677 = SGILog24
-     * 34712 = JPEG 2000
-     * 34713 = Nikon NEF Compressed
-     */
-    public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103;
-
-    /**
-     * 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;
-
-    /**
-     * 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 = 0x116;
-    /**
-     * The size of the raster data in bytes.
-     */
-    public static final int TAG_STRIP_BYTE_COUNTS = 0x0117;
-    /**
-     * 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_X_RESOLUTION = 0x011A;
-    public static final int TAG_Y_RESOLUTION = 0x011B;
-    public static final int TAG_PLANAR_CONFIGURATION = 0x011C;
-    public static final int TAG_RESOLUTION_UNIT = 0x0128;
     /**
      * The offset to thumbnail image bytes.
      */
@@ -132,40 +41,24 @@ public class ExifThumbnailDirectory extends Directory
      * The size of the thumbnail image data in bytes.
      */
     public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
-    public static final int TAG_YCBCR_COEFFICIENTS = 0x0211;
-    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;
+
+    /**
+     * @deprecated use {@link com.drew.metadata.exif.ExifDirectoryBase#TAG_COMPRESSION} instead.
+     */
+    @Deprecated
+    public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103;
 
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
-    static {
-        _tagNameMap.put(TAG_THUMBNAIL_IMAGE_WIDTH, "Thumbnail Image Width");
-        _tagNameMap.put(TAG_THUMBNAIL_IMAGE_HEIGHT, "Thumbnail Image Height");
-        _tagNameMap.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample");
-        _tagNameMap.put(TAG_THUMBNAIL_COMPRESSION, "Thumbnail Compression");
-        _tagNameMap.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation");
-        _tagNameMap.put(TAG_STRIP_OFFSETS, "Strip Offsets");
-        _tagNameMap.put(TAG_ORIENTATION, "Orientation");
-        _tagNameMap.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel");
-        _tagNameMap.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
-        _tagNameMap.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
-        _tagNameMap.put(TAG_X_RESOLUTION, "X Resolution");
-        _tagNameMap.put(TAG_Y_RESOLUTION, "Y Resolution");
-        _tagNameMap.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration");
-        _tagNameMap.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
+    static
+    {
+        addExifTagNames(_tagNameMap);
+
         _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
         _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
-        _tagNameMap.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
-        _tagNameMap.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
-        _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
-        _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
     }
 
-    @Nullable
-    private byte[] _thumbnailData;
-
     public ExifThumbnailDirectory()
     {
         this.setDescriptor(new ExifThumbnailDescriptor(this));
@@ -184,226 +77,4 @@ public class ExifThumbnailDirectory extends Directory
     {
         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/Source/com/drew/metadata/exif/ExifTiffHandler.java b/Source/com/drew/metadata/exif/ExifTiffHandler.java
index 2add05a..c02b60f 100644
--- a/Source/com/drew/metadata/exif/ExifTiffHandler.java
+++ b/Source/com/drew/metadata/exif/ExifTiffHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,16 +22,25 @@ package com.drew.metadata.exif;
 
 import com.drew.imaging.tiff.TiffProcessingException;
 import com.drew.imaging.tiff.TiffReader;
+import com.drew.imaging.jpeg.JpegMetadataReader;
+import com.drew.imaging.jpeg.JpegProcessingException;
+
+import com.drew.lang.Charsets;
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.SequentialByteArrayReader;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 import com.drew.metadata.exif.makernotes.*;
 import com.drew.metadata.iptc.IptcReader;
 import com.drew.metadata.tiff.DirectoryTiffHandler;
+import com.drew.metadata.xmp.XmpReader;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Set;
 
 /**
@@ -44,36 +53,91 @@ import java.util.Set;
  */
 public class ExifTiffHandler extends DirectoryTiffHandler
 {
-    private final boolean _storeThumbnailBytes;
-
-    public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes)
+    public ExifTiffHandler(@NotNull Metadata metadata, @Nullable Directory parentDirectory)
     {
-        super(metadata, ExifIFD0Directory.class);
-        _storeThumbnailBytes = storeThumbnailBytes;
+        super(metadata);
+
+        if (parentDirectory != null)
+            _currentDirectory.setParent(parentDirectory);
     }
 
     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 != panasonicRawTiffMarker) {
-            throw new TiffProcessingException("Unexpected TIFF marker: 0x" + Integer.toHexString(marker));
+        switch (marker)
+        {
+            case standardTiffMarker:
+            case olympusRawTiffMarker:      // Todo: implement an IFD0, if there is one
+            case olympusRawTiffMarker2:     // Todo: implement an IFD0, if there is one
+                pushDirectory(ExifIFD0Directory.class);
+                break;
+            case panasonicRawTiffMarker:
+                pushDirectory(PanasonicRawIFD0Directory.class);
+                break;
+            default:
+                throw new TiffProcessingException(String.format("Unexpected TIFF marker: 0x%X", marker));
         }
     }
 
-    public boolean isTagIfdPointer(int tagType)
+    public boolean tryEnterSubIfd(int tagId)
     {
-        if (tagType == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
+        if (tagId == ExifDirectoryBase.TAG_SUB_IFD_OFFSET) {
             pushDirectory(ExifSubIFDDirectory.class);
             return true;
-        } else if (tagType == ExifIFD0Directory.TAG_GPS_INFO_OFFSET && _currentDirectory instanceof ExifIFD0Directory) {
-            pushDirectory(GpsDirectory.class);
-            return true;
-        } else if (tagType == ExifSubIFDDirectory.TAG_INTEROP_OFFSET && _currentDirectory instanceof ExifSubIFDDirectory) {
-            pushDirectory(ExifInteropDirectory.class);
-            return true;
+        }
+
+        if (_currentDirectory instanceof ExifIFD0Directory || _currentDirectory instanceof PanasonicRawIFD0Directory) {
+            if (tagId == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET) {
+                pushDirectory(ExifSubIFDDirectory.class);
+                return true;
+            }
+
+            if (tagId == ExifIFD0Directory.TAG_GPS_INFO_OFFSET) {
+                pushDirectory(GpsDirectory.class);
+                return true;
+            }
+        }
+
+        if (_currentDirectory instanceof ExifSubIFDDirectory) {
+            if (tagId == ExifSubIFDDirectory.TAG_INTEROP_OFFSET) {
+                pushDirectory(ExifInteropDirectory.class);
+                return true;
+            }
+        }
+
+        if (_currentDirectory instanceof OlympusMakernoteDirectory) {
+            // Note: these also appear in customProcessTag because some are IFD pointers while others begin immediately
+            // for the same directories
+            switch(tagId) {
+                case OlympusMakernoteDirectory.TAG_EQUIPMENT:
+                    pushDirectory(OlympusEquipmentMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS:
+                    pushDirectory(OlympusCameraSettingsMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT:
+                    pushDirectory(OlympusRawDevelopmentMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT_2:
+                    pushDirectory(OlympusRawDevelopment2MakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_IMAGE_PROCESSING:
+                    pushDirectory(OlympusImageProcessingMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_FOCUS_INFO:
+                    pushDirectory(OlympusFocusInfoMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_INFO:
+                    pushDirectory(OlympusRawInfoMakernoteDirectory.class);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_MAIN_INFO:
+                    pushDirectory(OlympusMakernoteDirectory.class);
+                    return true;
+            }
         }
 
         return false;
@@ -82,8 +146,14 @@ public class ExifTiffHandler extends DirectoryTiffHandler
     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);
+        // UPDATE: In multipage TIFFs, the 'follower' IFD points to the next image in the set
+        if (_currentDirectory instanceof ExifIFD0Directory || _currentDirectory instanceof ExifImageDirectory) {
+            // If the PageNumber tag is defined, assume this is a multipage TIFF or similar
+            // TODO: Find better ways to know which follower Directory should be used
+            if (_currentDirectory.containsTag(ExifDirectoryBase.TAG_PAGE_NUMBER))
+                pushDirectory(ExifImageDirectory.class);
+            else
+                pushDirectory(ExifThumbnailDirectory.class);
             return true;
         }
 
@@ -96,6 +166,19 @@ public class ExifTiffHandler extends DirectoryTiffHandler
         return false;
     }
 
+    @Nullable
+    public Long tryCustomProcessFormat(final int tagId, final int formatCode, final long componentCount)
+    {
+        if (formatCode == 13)
+            return componentCount * 4;
+
+        // an unknown (0) formatCode needs to be potentially handled later as a highly custom directory tag
+        if(formatCode == 0)
+            return 0L;
+
+        return null;
+    }
+
     public boolean customProcessTag(final int tagOffset,
                                     final @NotNull Set<Integer> processedIfdOffsets,
                                     final int tiffHeaderOffset,
@@ -103,6 +186,20 @@ public class ExifTiffHandler extends DirectoryTiffHandler
                                     final int tagId,
                                     final int byteCount) throws IOException
     {
+        // Some 0x0000 tags have a 0 byteCount. Determine whether it's bad.
+        if (tagId == 0)
+        {
+            if (_currentDirectory.containsTag(tagId))
+            {
+                // Let it go through for now. Some directories handle it, some don't
+                return false;
+            }
+
+            // Skip over 0x0000 tags that don't have any associated bytes. No idea what it contains in this case, if anything.
+            if (byteCount == 0)
+                return true;
+        }
+
         // Custom processing for the Makernote tag
         if (tagId == ExifSubIFDDirectory.TAG_MAKERNOTE && _currentDirectory instanceof ExifSubIFDDirectory) {
             return processMakernote(tagOffset, processedIfdOffsets, tiffHeaderOffset, reader);
@@ -113,30 +210,157 @@ public class ExifTiffHandler extends DirectoryTiffHandler
             // 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);
+                new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length, _currentDirectory);
                 return true;
             }
             return false;
         }
 
+        // Custom processing for embedded XMP data
+        if (tagId == ExifSubIFDDirectory.TAG_APPLICATION_NOTES && _currentDirectory instanceof ExifIFD0Directory) {
+            new XmpReader().extract(reader.getNullTerminatedBytes(tagOffset, byteCount), _metadata, _currentDirectory);
+            return true;
+        }
+
+        if (HandlePrintIM(_currentDirectory, tagId))
+        {
+            PrintIMDirectory printIMDirectory = new PrintIMDirectory();
+            printIMDirectory.setParent(_currentDirectory);
+            _metadata.addDirectory(printIMDirectory);
+            ProcessPrintIM(printIMDirectory, tagOffset, reader, byteCount);
+            return true;
+        }
+
+        // Note: these also appear in tryEnterSubIfd because some are IFD pointers while others begin immediately
+        // for the same directories
+        if(_currentDirectory instanceof OlympusMakernoteDirectory)
+        {
+            switch (tagId)
+            {
+                case OlympusMakernoteDirectory.TAG_EQUIPMENT:
+                    pushDirectory(OlympusEquipmentMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS:
+                    pushDirectory(OlympusCameraSettingsMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT:
+                    pushDirectory(OlympusRawDevelopmentMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT_2:
+                    pushDirectory(OlympusRawDevelopment2MakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_IMAGE_PROCESSING:
+                    pushDirectory(OlympusImageProcessingMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_FOCUS_INFO:
+                    pushDirectory(OlympusFocusInfoMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_RAW_INFO:
+                    pushDirectory(OlympusRawInfoMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+                case OlympusMakernoteDirectory.TAG_MAIN_INFO:
+                    pushDirectory(OlympusMakernoteDirectory.class);
+                    TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset);
+                    return true;
+            }
+        }
+
+        if (_currentDirectory instanceof PanasonicRawIFD0Directory)
+        {
+            // these contain binary data with specific offsets, and can't be processed as regular ifd's.
+            // The binary data is broken into 'fake' tags and there is a pattern.
+            switch (tagId)
+            {
+                case PanasonicRawIFD0Directory.TagWbInfo:
+                    PanasonicRawWbInfoDirectory dirWbInfo = new PanasonicRawWbInfoDirectory();
+                    dirWbInfo.setParent(_currentDirectory);
+                    _metadata.addDirectory(dirWbInfo);
+                    ProcessBinary(dirWbInfo, tagOffset, reader, byteCount, false, 2);
+                    return true;
+                case PanasonicRawIFD0Directory.TagWbInfo2:
+                    PanasonicRawWbInfo2Directory dirWbInfo2 = new PanasonicRawWbInfo2Directory();
+                    dirWbInfo2.setParent(_currentDirectory);
+                    _metadata.addDirectory(dirWbInfo2);
+                    ProcessBinary(dirWbInfo2, tagOffset, reader, byteCount, false, 3);
+                    return true;
+                case PanasonicRawIFD0Directory.TagDistortionInfo:
+                    PanasonicRawDistortionDirectory dirDistort = new PanasonicRawDistortionDirectory();
+                    dirDistort.setParent(_currentDirectory);
+                    _metadata.addDirectory(dirDistort);
+                    ProcessBinary(dirDistort, tagOffset, reader, byteCount, true, 1);
+                    return true;
+            }
+        }
+
+        // Panasonic RAW sometimes contains an embedded version of the data as a JPG file.
+        if (tagId == PanasonicRawIFD0Directory.TagJpgFromRaw && _currentDirectory instanceof PanasonicRawIFD0Directory)
+        {
+            byte[] jpegrawbytes = reader.getBytes(tagOffset, byteCount);
+
+            // Extract information from embedded image since it is metadata-rich
+            ByteArrayInputStream jpegmem = new ByteArrayInputStream(jpegrawbytes);
+            try {
+                Metadata jpegDirectory = JpegMetadataReader.readMetadata(jpegmem);
+                for (Directory directory : jpegDirectory.getDirectories()) {
+                    directory.setParent(_currentDirectory);
+                    _metadata.addDirectory(directory);
+                }
+                return true;
+            } catch (JpegProcessingException e) {
+                _currentDirectory.addError("Error processing JpgFromRaw: " + e.getMessage());
+            } catch (IOException e) {
+                _currentDirectory.addError("Error reading JpgFromRaw: " + e.getMessage());
+            }
+        }
+
         return false;
     }
 
-    public void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset)
+    private static void ProcessBinary(@NotNull final Directory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader, final int byteCount, final Boolean issigned, final int arrayLength) throws IOException
     {
-        if (_storeThumbnailBytes) {
-            // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
-            ExifThumbnailDirectory thumbnailDirectory = _metadata.getDirectory(ExifThumbnailDirectory.class);
-            if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
-                Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
-                Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
-                if (offset != null && length != null) {
-                    try {
-                        byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);
-                        thumbnailDirectory.setThumbnailData(thumbnailData);
-                    } catch (IOException ex) {
-                        thumbnailDirectory.addError("Invalid thumbnail data specification: " + ex.getMessage());
+        // expects signed/unsigned int16 (for now)
+        //int byteSize = issigned ? sizeof(short) : sizeof(ushort);
+        int byteSize = 2;
+
+        // 'directory' is assumed to contain tags that correspond to the byte position unless it's a set of bytes
+        for (int i = 0; i < byteCount; i++)
+        {
+            if (directory.hasTagName(i))
+            {
+                // only process this tag if the 'next' integral tag exists. Otherwise, it's a set of bytes
+                if (i < byteCount - 1 && directory.hasTagName(i + 1))
+                {
+                    if(issigned)
+                        directory.setObject(i, reader.getInt16(tagValueOffset + (i* byteSize)));
+                    else
+                        directory.setObject(i, reader.getUInt16(tagValueOffset + (i* byteSize)));
+                }
+                else
+                {
+                    // the next arrayLength bytes are a multi-byte value
+                    if (issigned)
+                    {
+                        short[] val = new short[arrayLength];
+                        for (int j = 0; j<val.length; j++)
+                            val[j] = reader.getInt16(tagValueOffset + ((i + j) * byteSize));
+                        directory.setObjectArray(i, val);
+                    }
+                    else
+                    {
+                        int[] val = new int[arrayLength];
+                        for (int j = 0; j<val.length; j++)
+                            val[j] = reader.getUInt16(tagValueOffset + ((i + j) * byteSize));
+                        directory.setObjectArray(i, val);
                     }
+
+                    i += arrayLength - 1;
                 }
             }
         }
@@ -148,29 +372,34 @@ public class ExifTiffHandler extends DirectoryTiffHandler
                                      final @NotNull RandomAccessReader reader) throws IOException
     {
         // Determine the camera model and makernote format.
-        Directory ifd0Directory = _metadata.getDirectory(ExifIFD0Directory.class);
-
-        if (ifd0Directory == null)
-            return false;
+        Directory ifd0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
 
-        String cameraMake = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
+        String cameraMake = ifd0Directory == null ? null : 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);
+        final String firstTwoChars    = reader.getString(makernoteOffset, 2, Charsets.UTF_8);
+        final String firstThreeChars  = reader.getString(makernoteOffset, 3, Charsets.UTF_8);
+        final String firstFourChars   = reader.getString(makernoteOffset, 4, Charsets.UTF_8);
+        final String firstFiveChars   = reader.getString(makernoteOffset, 5, Charsets.UTF_8);
+        final String firstSixChars    = reader.getString(makernoteOffset, 6, Charsets.UTF_8);
+        final String firstSevenChars  = reader.getString(makernoteOffset, 7, Charsets.UTF_8);
+        final String firstEightChars  = reader.getString(makernoteOffset, 8, Charsets.UTF_8);
+        final String firstNineChars   = reader.getString(makernoteOffset, 9, Charsets.UTF_8);
+        final String firstTenChars    = reader.getString(makernoteOffset, 10, Charsets.UTF_8);
+        final String firstTwelveChars = reader.getString(makernoteOffset, 12, Charsets.UTF_8);
 
         boolean byteOrderBefore = reader.isMotorolaByteOrder();
 
-        if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
+        if ("OLYMP\0".equals(firstSixChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
             // Olympus Makernote
             // Epson and Agfa use Olympus makernote standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/
             pushDirectory(OlympusMakernoteDirectory.class);
             TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
+        } else if ("OLYMPUS\0II".equals(firstTenChars)) {
+            // Olympus Makernote (alternate)
+            // Note that data is relative to the beginning of the makernote
+            // http://exiv2.org/makernote.html
+            pushDirectory(OlympusMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, makernoteOffset);
         } else if (cameraMake != null && cameraMake.toUpperCase().startsWith("MINOLTA")) {
             // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote
             // area that commences immediately.
@@ -196,7 +425,7 @@ public class ExifTiffHandler extends DirectoryTiffHandler
                         TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 18, makernoteOffset + 10);
                         break;
                     default:
-                        ifd0Directory.addError("Unsupported Nikon makernote data ignored.");
+                        _currentDirectory.addError("Unsupported Nikon makernote data ignored.");
                         break;
                 }
             } else {
@@ -207,6 +436,12 @@ public class ExifTiffHandler extends DirectoryTiffHandler
         } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {
             pushDirectory(SonyType1MakernoteDirectory.class);
             TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset);
+        // Do this check LAST after most other Sony checks
+        } else if (cameraMake != null && cameraMake.startsWith("SONY") &&
+                !Arrays.equals(reader.getBytes(makernoteOffset, 2), new byte[]{ 0x01, 0x00 }) ) {
+            // The IFD begins with the first Makernote byte (no ASCII name). Used in SR2 and ARW images
+            pushDirectory(SonyType1MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
         } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
             // force MM for this directory
             reader.setMotorolaByteOrder(true);
@@ -218,7 +453,9 @@ public class ExifTiffHandler extends DirectoryTiffHandler
             TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 10, tiffHeaderOffset);
         } else if ("KDK".equals(firstThreeChars)) {
             reader.setMotorolaByteOrder(firstSevenChars.equals("KDK INFO"));
-            processKodakMakernote(_metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), makernoteOffset, reader);
+            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);
@@ -245,7 +482,23 @@ public class ExifTiffHandler extends DirectoryTiffHandler
             TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 22, tiffHeaderOffset);
         } else if ("LEICA".equals(firstFiveChars)) {
             reader.setMotorolaByteOrder(false);
-            if ("Leica Camera AG".equals(cameraMake)) {
+
+            // used by the X1/X2/X VARIO/T
+            // (X1 starts with "LEICA\0\x01\0", Make is "LEICA CAMERA AG")
+            // (X2 starts with "LEICA\0\x05\0", Make is "LEICA CAMERA AG")
+            // (X VARIO starts with "LEICA\0\x04\0", Make is "LEICA CAMERA AG")
+            // (T (Typ 701) starts with "LEICA\0\0x6", Make is "LEICA CAMERA AG")
+            // (X (Typ 113) starts with "LEICA\0\0x7", Make is "LEICA CAMERA AG")
+
+            if ("LEICA\0\u0001\0".equals(firstEightChars) ||
+                "LEICA\0\u0004\0".equals(firstEightChars) ||
+                "LEICA\0\u0005\0".equals(firstEightChars) ||
+                "LEICA\0\u0006\0".equals(firstEightChars) ||
+                "LEICA\0\u0007\0".equals(firstEightChars))
+            {
+                pushDirectory(LeicaType5MakernoteDirectory.class);
+                TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
+            } else if ("Leica Camera AG".equals(cameraMake)) {
                 pushDirectory(LeicaMakernoteDirectory.class);
                 TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset);
             } else if ("LEICA".equals(cameraMake)) {
@@ -255,7 +508,7 @@ public class ExifTiffHandler extends DirectoryTiffHandler
             } else {
                 return false;
             }
-        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12))) {
+        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12, Charsets.UTF_8))) {
             // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD
             // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment
             // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html
@@ -300,6 +553,25 @@ public class ExifTiffHandler extends DirectoryTiffHandler
                 pushDirectory(RicohMakernoteDirectory.class);
                 TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset);
             }
+        } else if (firstTenChars.equals("Apple iOS\0")) {
+            // Always in Motorola byte order
+            boolean orderBefore = reader.isMotorolaByteOrder();
+            reader.setMotorolaByteOrder(true);
+            pushDirectory(AppleMakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 14, makernoteOffset);
+            reader.setMotorolaByteOrder(orderBefore);
+        } else if (reader.getUInt16(makernoteOffset) == ReconyxHyperFireMakernoteDirectory.MAKERNOTE_VERSION) {
+            ReconyxHyperFireMakernoteDirectory directory = new ReconyxHyperFireMakernoteDirectory();
+            _metadata.addDirectory(directory);
+            processReconyxHyperFireMakernote(directory, makernoteOffset, reader);
+        } else if (firstNineChars.equalsIgnoreCase("RECONYXUF")) {
+            ReconyxUltraFireMakernoteDirectory directory = new ReconyxUltraFireMakernoteDirectory();
+            _metadata.addDirectory(directory);
+            processReconyxUltraFireMakernote(directory, makernoteOffset, reader);
+        } else if ("SAMSUNG".equals(cameraMake)) {
+            // Only handles Type2 notes correctly. Others aren't implemented, and it's complex to determine which ones to use
+            pushDirectory(SamsungType2MakernoteDirectory.class);
+            TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset);
         } else {
             // The makernote is not comprehended by this library.
             // If you are reading this and believe a particular camera's image should be processed, get in touch.
@@ -310,12 +582,97 @@ public class ExifTiffHandler extends DirectoryTiffHandler
         return true;
     }
 
+    private static Boolean HandlePrintIM(@NotNull final Directory directory, final int tagId)
+    {
+        if (tagId == ExifDirectoryBase.TAG_PRINT_IMAGE_MATCHING_INFO)
+            return true;
+
+        if (tagId == 0x0E00)
+        {
+            // Tempting to say every tagid of 0x0E00 is a PIM tag, but can't be 100% sure
+            if (directory instanceof CasioType2MakernoteDirectory ||
+                directory instanceof KyoceraMakernoteDirectory ||
+                directory instanceof NikonType2MakernoteDirectory ||
+                directory instanceof OlympusMakernoteDirectory ||
+                directory instanceof PanasonicMakernoteDirectory ||
+                directory instanceof PentaxMakernoteDirectory ||
+                directory instanceof RicohMakernoteDirectory ||
+                directory instanceof SanyoMakernoteDirectory ||
+                directory instanceof SonyType1MakernoteDirectory)
+                return true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Process PrintIM IFD
+    /// </summary>
+    /// <remarks>
+    /// Converted from Exiftool version 10.33 created by Phil Harvey
+    /// http://www.sno.phy.queensu.ca/~phil/exiftool/
+    /// lib\Image\ExifTool\PrintIM.pm
+    /// </remarks>
+    private static void ProcessPrintIM(@NotNull final PrintIMDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader, final int byteCount) throws IOException
+    {
+        Boolean resetByteOrder = null;
+
+        if (byteCount == 0)
+        {
+            directory.addError("Empty PrintIM data");
+            return;
+        }
+
+        if (byteCount <= 15)
+        {
+            directory.addError("Bad PrintIM data");
+            return;
+        }
+
+        String header = reader.getString(tagValueOffset, 12, Charsets.UTF_8);
+
+        if (!header.startsWith("PrintIM")) //, StringComparison.Ordinal))
+        {
+            directory.addError("Invalid PrintIM header");
+            return;
+        }
+
+        // check size of PrintIM block
+        int num = reader.getUInt16(tagValueOffset + 14);
+        if (byteCount < 16 + num * 6)
+        {
+            // size is too big, maybe byte ordering is wrong
+            resetByteOrder = reader.isMotorolaByteOrder();
+            reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder());
+            num = reader.getUInt16(tagValueOffset + 14);
+            if (byteCount < 16 + num * 6)
+            {
+                directory.addError("Bad PrintIM size");
+                return;
+            }
+        }
+
+        directory.setObject(PrintIMDirectory.TagPrintImVersion, header.substring(8, 12));
+
+        for (int n = 0; n < num; n++)
+        {
+            int pos = tagValueOffset + 16 + n * 6;
+            int tag = reader.getUInt16(pos);
+            long val = reader.getUInt32(pos + 2);
+
+            directory.setObject(tag, val);
+        }
+
+        if (resetByteOrder != null)
+            reader.setMotorolaByteOrder(resetByteOrder);
+    }
+
     private static void processKodakMakernote(@NotNull final KodakMakernoteDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader)
     {
         // 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.setStringValue(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getStringValue(dataOffset, 8, Charsets.UTF_8));
             directory.setInt(KodakMakernoteDirectory.TAG_QUALITY, reader.getUInt8(dataOffset + 9));
             directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE, reader.getUInt8(dataOffset + 10));
             directory.setInt(KodakMakernoteDirectory.TAG_IMAGE_WIDTH, reader.getUInt16(dataOffset + 12));
@@ -345,5 +702,148 @@ public class ExifTiffHandler extends DirectoryTiffHandler
             directory.addError("Error processing Kodak makernote data: " + ex.getMessage());
         }
     }
+
+    private static void processReconyxHyperFireMakernote(@NotNull final ReconyxHyperFireMakernoteDirectory directory, final int makernoteOffset, @NotNull final RandomAccessReader reader) throws IOException
+    {
+        directory.setObject(ReconyxHyperFireMakernoteDirectory.TAG_MAKERNOTE_VERSION, reader.getUInt16(makernoteOffset));
+
+        int major = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION);
+        int minor = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 2);
+        int revision = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 4);
+        String buildYear = String.format("%04X", reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 6));
+        String buildDate = String.format("%04X", reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 8));
+        String buildYearAndDate = buildYear + buildDate;
+        Integer build;
+        try {
+            build = Integer.parseInt(buildYearAndDate);
+        } catch (NumberFormatException e) {
+            build = null;
+        }
+        if (build != null)
+        {
+            directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d.%s", major, minor, revision, build));
+        }
+        else
+        {
+            directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d", major, minor, revision));
+            directory.addError("Error processing Reconyx HyperFire makernote data: build '" + buildYearAndDate + "' is not in the expected format and will be omitted from Firmware Version.");
+        }
+
+        directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_TRIGGER_MODE, String.valueOf((char)reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_TRIGGER_MODE)));
+        directory.setIntArray(ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE,
+                      new int[]
+                      {
+                          reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE),
+                          reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE + 2)
+                      });
+
+        int eventNumberHigh = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER);
+        int eventNumberLow = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER + 2);
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER, (eventNumberHigh << 16) + eventNumberLow);
+
+        int seconds = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL);
+        int minutes = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 2);
+        int hour = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 4);
+        int month = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 6);
+        int day = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 8);
+        int year = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 10);
+
+        if ((seconds >= 0 && seconds < 60) &&
+            (minutes >= 0 && minutes < 60) &&
+            (hour >= 0 && hour < 24) &&
+            (month >= 1 && month < 13) &&
+            (day >= 1 && day < 32) &&
+            (year >= 1 && year <= 9999))
+        {
+            directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL,
+                    String.format("%4d:%2d:%2d %2d:%2d:%2d", year, month, day, hour, minutes, seconds));
+        }
+        else
+        {
+            directory.addError("Error processing Reconyx HyperFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time.");
+        }
+
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_MOON_PHASE, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_MOON_PHASE));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, reader.getInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE_FAHRENHEIT));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE, reader.getInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE));
+        //directory.setByteArray(ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, reader.getBytes(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, 28));
+        directory.setStringValue(ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, new StringValue(reader.getBytes(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, 28), Charsets.UTF_16LE));
+        // two unread bytes: the serial number's terminating null
+
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_CONTRAST, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_CONTRAST));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_BRIGHTNESS, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_BRIGHTNESS));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_SHARPNESS, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SHARPNESS));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_SATURATION, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SATURATION));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_INFRARED_ILLUMINATOR, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_INFRARED_ILLUMINATOR));
+        directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_MOTION_SENSITIVITY, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_MOTION_SENSITIVITY));
+        directory.setDouble(ReconyxHyperFireMakernoteDirectory.TAG_BATTERY_VOLTAGE, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_BATTERY_VOLTAGE) / 1000.0);
+        directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_USER_LABEL, reader.getNullTerminatedString(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_USER_LABEL, 44, Charsets.UTF_8));
+    }
+
+    private static void processReconyxUltraFireMakernote(@NotNull final ReconyxUltraFireMakernoteDirectory directory, final int makernoteOffset, @NotNull final RandomAccessReader reader) throws IOException
+    {
+        directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_LABEL, reader.getString(makernoteOffset, 9, Charsets.UTF_8));
+        /*uint makernoteID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteID));
+        directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteID, makernoteID);
+        if (makernoteID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_ID)
+        {
+            directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote ID 0x" + makernoteID.ToString("x8"));
+            return;
+        }
+        directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteSize, ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteSize)));
+        uint makernotePublicID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID));
+        directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID, makernotePublicID);
+        if (makernotePublicID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_PUBLIC_ID)
+        {
+            directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote Public ID 0x" + makernotePublicID.ToString("x8"));
+            return;
+        }*/
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicSize, ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicSize)));
+
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagCameraVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagCameraVersion, reader));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagUibVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagUibVersion, reader));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagBtlVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagBtlVersion, reader));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagPexVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagPexVersion, reader));
+
+        directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_EVENT_TYPE, reader.getString(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_EVENT_TYPE, 1, Charsets.UTF_8));
+        directory.setIntArray(ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE,
+                      new int[]
+                      {
+                          reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE),
+                          reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE + 1)
+                      });
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagEventNumber, ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagEventNumber)));
+
+        byte seconds = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL);
+        byte minutes = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 1);
+        byte hour = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 2);
+        byte day = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 3);
+        byte month = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 4);
+        /*ushort year = ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagDateTimeOriginal + 5));
+        if ((seconds >= 0 && seconds < 60) &&
+            (minutes >= 0 && minutes < 60) &&
+            (hour >= 0 && hour < 24) &&
+            (month >= 1 && month < 13) &&
+            (day >= 1 && day < 32) &&
+            (year >= 1 && year <= 9999))
+        {
+            directory.Set(ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL, new DateTime(year, month, day, hour, minutes, seconds, DateTimeKind.Unspecified));
+        }
+        else
+        {
+            directory.addError("Error processing Reconyx UltraFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time.");
+        }*/
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagDayOfWeek, reader.GetByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagDayOfWeek));
+
+        directory.setInt(ReconyxUltraFireMakernoteDirectory.TAG_MOON_PHASE, reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_MOON_PHASE));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagAmbientTemperatureFahrenheit, ByteConvert.FromBigEndianToNative(reader.GetInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagAmbientTemperatureFahrenheit)));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagAmbientTemperature, ByteConvert.FromBigEndianToNative(reader.GetInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagAmbientTemperature)));
+
+        directory.setInt(ReconyxUltraFireMakernoteDirectory.TAG_FLASH, reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_FLASH));
+        //directory.Set(ReconyxUltraFireMakernoteDirectory.TagBatteryVoltage, ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagBatteryVoltage)) / 1000.0);
+        directory.setStringValue(ReconyxUltraFireMakernoteDirectory.TAG_SERIAL_NUMBER, new StringValue(reader.getBytes(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SERIAL_NUMBER, 14), Charsets.UTF_8));
+        // unread byte: the serial number's terminating null
+        directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_USER_LABEL, reader.getNullTerminatedString(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_USER_LABEL, 20, Charsets.UTF_8));
+    }
 }
 
diff --git a/Source/com/drew/metadata/exif/GpsDescriptor.java b/Source/com/drew/metadata/exif/GpsDescriptor.java
index 4cdae31..0887a35 100644
--- a/Source/com/drew/metadata/exif/GpsDescriptor.java
+++ b/Source/com/drew/metadata/exif/GpsDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import static com.drew.metadata.exif.GpsDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class GpsDescriptor extends TagDescriptor<GpsDirectory>
 {
     public GpsDescriptor(@NotNull GpsDirectory directory)
@@ -108,8 +109,14 @@ public class GpsDescriptor extends TagDescriptor<GpsDirectory>
     public String getGpsTimeStampDescription()
     {
         // time in hour, min, sec
-        int[] timeComponents = _directory.getIntArray(TAG_TIME_STAMP);
-        return timeComponents == null ? null : String.format("%d:%d:%d UTC", timeComponents[0], timeComponents[1], timeComponents[2]);
+        Rational[] timeComponents = _directory.getRationalArray(TAG_TIME_STAMP);
+        DecimalFormat df = new DecimalFormat("00.000");
+        return timeComponents == null
+            ? null
+            : String.format("%02d:%02d:%s UTC",
+                timeComponents[0].intValue(),
+                timeComponents[1].intValue(),
+                df.format(timeComponents[2].doubleValue()));
     }
 
     @Nullable
diff --git a/Source/com/drew/metadata/exif/GpsDirectory.java b/Source/com/drew/metadata/exif/GpsDirectory.java
index cdaad00..e9ce932 100644
--- a/Source/com/drew/metadata/exif/GpsDirectory.java
+++ b/Source/com/drew/metadata/exif/GpsDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,16 +24,21 @@ 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.Directory;
 
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.Locale;
 
 /**
  * Describes Exif tags that contain Global Positioning System (GPS) data.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class GpsDirectory extends Directory
+@SuppressWarnings("WeakerAccess")
+public class GpsDirectory extends ExifDirectoryBase
 {
     /** GPS tag version GPSVersionID 0 0 BYTE 4 */
     public static final int TAG_VERSION_ID = 0x0000;
@@ -101,6 +106,8 @@ public class GpsDirectory extends Directory
 
     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");
@@ -162,10 +169,10 @@ public class GpsDirectory extends Directory
     @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);
+        Rational[] latitudes = getRationalArray(TAG_LATITUDE);
+        Rational[] longitudes = getRationalArray(TAG_LONGITUDE);
+        String latitudeRef = getString(TAG_LATITUDE_REF);
+        String longitudeRef = getString(TAG_LONGITUDE_REF);
 
         // Make sure we have the required values
         if (latitudes == null || latitudes.length != 3)
@@ -184,4 +191,32 @@ public class GpsDirectory extends Directory
 
         return new GeoLocation(lat, lon);
     }
+
+    /**
+     * Parses the date stamp tag and the time stamp tag to obtain a single Date object representing the
+     * date and time when this image was captured.
+     *
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getGpsDate()
+    {
+        String date = getString(TAG_DATE_STAMP);
+        Rational[] timeComponents = getRationalArray(TAG_TIME_STAMP);
+
+        // Make sure we have the required values
+        if (date == null)
+            return null;
+        if (timeComponents == null || timeComponents.length != 3)
+            return null;
+
+        String dateTime = String.format(Locale.US, "%s %02d:%02d:%02.3f UTC",
+            date, timeComponents[0].intValue(), timeComponents[1].intValue(), timeComponents[2].doubleValue());
+        try {
+            DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss.S z");
+            return parser.parse(dateTime);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
 }
diff --git a/Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java b/Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java
new file mode 100644
index 0000000..dd08840
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawDistortionDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawDistortionDirectory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawDistortionDescriptor extends TagDescriptor<PanasonicRawDistortionDirectory>
+{
+    public PanasonicRawDistortionDescriptor(@NotNull PanasonicRawDistortionDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagDistortionParam02:
+                return getDistortionParam02Description();
+            case TagDistortionParam04:
+                return getDistortionParam04Description();
+            case TagDistortionScale:
+                return getDistortionScaleDescription();
+            case TagDistortionCorrection:
+                return getDistortionCorrectionDescription();
+            case TagDistortionParam08:
+                return getDistortionParam08Description();
+            case TagDistortionParam09:
+                return getDistortionParam09Description();
+            case TagDistortionParam11:
+                return getDistortionParam11Description();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getWbTypeDescription(int tagType)
+    {
+        Integer wbtype = _directory.getInteger(tagType);
+        if (wbtype == null)
+            return null;
+
+        return super.getLightSourceDescription(wbtype.shortValue());
+    }
+
+    @Nullable
+    public String getDistortionParam02Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam02);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionParam04Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam04);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionScaleDescription()
+    {
+        Integer value = _directory.getInteger(TagDistortionScale);
+        if (value == null)
+            return null;
+
+        //return (1 / (1 + value / 32768)).toString();
+        return Integer.toString(1 / (1 + value / 32768));
+    }
+
+    @Nullable
+    public String getDistortionCorrectionDescription()
+    {
+        Integer value = _directory.getInteger(TagDistortionCorrection);
+        if (value == null)
+            return null;
+
+        // (have seen the upper 4 bits set for GF5 and GX1, giving a value of -4095 - PH)
+        int mask = 0x000f;
+        switch (value & mask)
+        {
+            case 0:
+                return "Off";
+            case 1:
+                return "On";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getDistortionParam08Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam08);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionParam09Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam09);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+
+    @Nullable
+    public String getDistortionParam11Description()
+    {
+        Integer value = _directory.getInteger(TagDistortionParam11);
+        if (value == null)
+            return null;
+
+        return new Rational(value, 32678).toString();
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java b/Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java
new file mode 100644
index 0000000..c86e664
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawDistortionDirectory extends Directory
+{
+    // 0 and 1 are checksums
+
+    public static final int TagDistortionParam02 = 2;
+
+    public static final int TagDistortionParam04 = 4;
+    public static final int TagDistortionScale = 5;
+
+    public static final int TagDistortionCorrection = 7;
+    public static final int TagDistortionParam08 = 8;
+    public static final int TagDistortionParam09 = 9;
+
+    public static final int TagDistortionParam11 = 11;
+    public static final int TagDistortionN = 12;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagDistortionParam02, "Distortion Param 2");
+        _tagNameMap.put(TagDistortionParam04, "Distortion Param 4");
+        _tagNameMap.put(TagDistortionScale, "Distortion Scale");
+        _tagNameMap.put(TagDistortionCorrection, "Distortion Correction");
+        _tagNameMap.put(TagDistortionParam08, "Distortion Param 8");
+        _tagNameMap.put(TagDistortionParam09, "Distortion Param 9");
+        _tagNameMap.put(TagDistortionParam11, "Distortion Param 11");
+        _tagNameMap.put(TagDistortionN, "Distortion N");
+    }
+
+    public PanasonicRawDistortionDirectory()
+    {
+        this.setDescriptor(new PanasonicRawDistortionDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw DistortionInfo";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java b/Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java
new file mode 100644
index 0000000..61efbac
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawIFD0Directory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawIFD0Directory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawIFD0Descriptor extends TagDescriptor<PanasonicRawIFD0Directory>
+{
+    public PanasonicRawIFD0Descriptor(@NotNull PanasonicRawIFD0Directory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType)
+        {
+            case TagPanasonicRawVersion:
+                return getVersionBytesDescription(TagPanasonicRawVersion, 2);
+            case TagOrientation:
+                return getOrientationDescription(TagOrientation);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java b/Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java
new file mode 100644
index 0000000..bc94052
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags are found in IFD0 of Panasonic/Leica RAW, RW2 and RWL images.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawIFD0Directory extends Directory
+{
+    public static final int TagPanasonicRawVersion = 0x0001;
+    public static final int TagSensorWidth = 0x0002;
+    public static final int TagSensorHeight = 0x0003;
+    public static final int TagSensorTopBorder = 0x0004;
+    public static final int TagSensorLeftBorder = 0x0005;
+    public static final int TagSensorBottomBorder = 0x0006;
+    public static final int TagSensorRightBorder = 0x0007;
+
+    public static final int TagBlackLevel1 = 0x0008;
+    public static final int TagBlackLevel2 = 0x0009;
+    public static final int TagBlackLevel3 = 0x000a;
+    public static final int TagLinearityLimitRed = 0x000e;
+    public static final int TagLinearityLimitGreen = 0x000f;
+    public static final int TagLinearityLimitBlue = 0x0010;
+    public static final int TagRedBalance = 0x0011;
+    public static final int TagBlueBalance = 0x0012;
+    public static final int TagWbInfo = 0x0013;
+
+    public static final int TagIso = 0x0017;
+    public static final int TagHighIsoMultiplierRed = 0x0018;
+    public static final int TagHighIsoMultiplierGreen = 0x0019;
+    public static final int TagHighIsoMultiplierBlue = 0x001a;
+    public static final int TagBlackLevelRed = 0x001c;
+    public static final int TagBlackLevelGreen = 0x001d;
+    public static final int TagBlackLevelBlue = 0x001e;
+    public static final int TagWbRedLevel = 0x0024;
+    public static final int TagWbGreenLevel = 0x0025;
+    public static final int TagWbBlueLevel = 0x0026;
+
+    public static final int TagWbInfo2 = 0x0027;
+
+    public static final int TagJpgFromRaw = 0x002e;
+
+    public static final int TagCropTop = 0x002f;
+    public static final int TagCropLeft = 0x0030;
+    public static final int TagCropBottom = 0x0031;
+    public static final int TagCropRight = 0x0032;
+
+    public static final int TagMake = 0x010f;
+    public static final int TagModel = 0x0110;
+    public static final int TagStripOffsets = 0x0111;
+    public static final int TagOrientation = 0x0112;
+    public static final int TagRowsPerStrip = 0x0116;
+    public static final int TagStripByteCounts = 0x0117;
+    public static final int TagRawDataOffset = 0x0118;
+
+    public static final int TagDistortionInfo = 0x0119;
+
+    public PanasonicRawIFD0Directory()
+    {
+        this.setDescriptor(new PanasonicRawIFD0Descriptor(this));
+    }
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagPanasonicRawVersion, "Panasonic Raw Version");
+        _tagNameMap.put(TagSensorWidth, "Sensor Width");
+        _tagNameMap.put(TagSensorHeight, "Sensor Height");
+        _tagNameMap.put(TagSensorTopBorder, "Sensor Top Border");
+        _tagNameMap.put(TagSensorLeftBorder, "Sensor Left Border");
+        _tagNameMap.put(TagSensorBottomBorder, "Sensor Bottom Border");
+        _tagNameMap.put(TagSensorRightBorder, "Sensor Right Border");
+
+        _tagNameMap.put(TagBlackLevel1, "Black Level 1");
+        _tagNameMap.put(TagBlackLevel2, "Black Level 2");
+        _tagNameMap.put(TagBlackLevel3, "Black Level 3");
+        _tagNameMap.put(TagLinearityLimitRed, "Linearity Limit Red");
+        _tagNameMap.put(TagLinearityLimitGreen, "Linearity Limit Green");
+        _tagNameMap.put(TagLinearityLimitBlue, "Linearity Limit Blue");
+        _tagNameMap.put(TagRedBalance, "Red Balance");
+        _tagNameMap.put(TagBlueBalance, "Blue Balance");
+
+        _tagNameMap.put(TagIso, "ISO");
+        _tagNameMap.put(TagHighIsoMultiplierRed, "High ISO Multiplier Red");
+        _tagNameMap.put(TagHighIsoMultiplierGreen, "High ISO Multiplier Green");
+        _tagNameMap.put(TagHighIsoMultiplierBlue, "High ISO Multiplier Blue");
+        _tagNameMap.put(TagBlackLevelRed, "Black Level Red");
+        _tagNameMap.put(TagBlackLevelGreen, "Black Level Green");
+        _tagNameMap.put(TagBlackLevelBlue, "Black Level Blue");
+        _tagNameMap.put(TagWbRedLevel, "WB Red Level");
+        _tagNameMap.put(TagWbGreenLevel, "WB Green Level");
+        _tagNameMap.put(TagWbBlueLevel, "WB Blue Level");
+
+        _tagNameMap.put(TagJpgFromRaw, "Jpg From Raw");
+
+        _tagNameMap.put(TagCropTop, "Crop Top");
+        _tagNameMap.put(TagCropLeft, "Crop Left");
+        _tagNameMap.put(TagCropBottom, "Crop Bottom");
+        _tagNameMap.put(TagCropRight, "Crop Right");
+
+        _tagNameMap.put(TagMake, "Make");
+        _tagNameMap.put(TagModel, "Model");
+        _tagNameMap.put(TagStripOffsets, "Strip Offsets");
+        _tagNameMap.put(TagOrientation, "Orientation");
+        _tagNameMap.put(TagRowsPerStrip, "Rows Per Strip");
+        _tagNameMap.put(TagStripByteCounts, "Strip Byte Counts");
+        _tagNameMap.put(TagRawDataOffset, "Raw Data Offset");
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw Exif IFD0";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java
new file mode 100644
index 0000000..60af647
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawWbInfo2Directory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawWbInfo2Directory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfo2Descriptor extends TagDescriptor<PanasonicRawWbInfo2Directory>
+{
+    public PanasonicRawWbInfo2Descriptor(@NotNull PanasonicRawWbInfo2Directory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagWbType1:
+            case TagWbType2:
+            case TagWbType3:
+            case TagWbType4:
+            case TagWbType5:
+            case TagWbType6:
+            case TagWbType7:
+                return getWbTypeDescription(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getWbTypeDescription(int tagType)
+    {
+        Integer wbtype = _directory.getInteger(tagType);
+        if (wbtype == null)
+            return null;
+
+        return super.getLightSourceDescription(wbtype.shortValue());
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java
new file mode 100644
index 0000000..2f29ab1
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfo2Directory extends Directory
+{
+    public static final int TagNumWbEntries = 0;
+
+    public static final int TagWbType1 = 1;
+    public static final int TagWbRgbLevels1 = 2;
+
+    public static final int TagWbType2 = 5;
+    public static final int TagWbRgbLevels2 = 6;
+
+    public static final int TagWbType3 = 9;
+    public static final int TagWbRgbLevels3 = 10;
+
+    public static final int TagWbType4 = 13;
+    public static final int TagWbRgbLevels4 = 14;
+
+    public static final int TagWbType5 = 17;
+    public static final int TagWbRgbLevels5 = 18;
+
+    public static final int TagWbType6 = 21;
+    public static final int TagWbRgbLevels6 = 22;
+
+    public static final int TagWbType7 = 25;
+    public static final int TagWbRgbLevels7 = 26;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagNumWbEntries, "Num WB Entries");
+        _tagNameMap.put(TagNumWbEntries, "Num WB Entries");
+        _tagNameMap.put(TagWbType1, "WB Type 1");
+        _tagNameMap.put(TagWbRgbLevels1, "WB RGB Levels 1");
+        _tagNameMap.put(TagWbType2, "WB Type 2");
+        _tagNameMap.put(TagWbRgbLevels2, "WB RGB Levels 2");
+        _tagNameMap.put(TagWbType3, "WB Type 3");
+        _tagNameMap.put(TagWbRgbLevels3, "WB RGB Levels 3");
+        _tagNameMap.put(TagWbType4, "WB Type 4");
+        _tagNameMap.put(TagWbRgbLevels4, "WB RGB Levels 4");
+        _tagNameMap.put(TagWbType5, "WB Type 5");
+        _tagNameMap.put(TagWbRgbLevels5, "WB RGB Levels 5");
+        _tagNameMap.put(TagWbType6, "WB Type 6");
+        _tagNameMap.put(TagWbRgbLevels6, "WB RGB Levels 6");
+        _tagNameMap.put(TagWbType7, "WB Type 7");
+        _tagNameMap.put(TagWbRgbLevels7, "WB RGB Levels 7");
+    }
+
+    public PanasonicRawWbInfo2Directory()
+    {
+        this.setDescriptor(new PanasonicRawWbInfo2Descriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw WbInfo2";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java
new file mode 100644
index 0000000..61ff4a2
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PanasonicRawWbInfoDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PanasonicRawWbInfoDirectory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfoDescriptor extends TagDescriptor<PanasonicRawWbInfoDirectory>
+{
+    public PanasonicRawWbInfoDescriptor(@NotNull PanasonicRawWbInfoDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagWbType1:
+            case TagWbType2:
+            case TagWbType3:
+            case TagWbType4:
+            case TagWbType5:
+            case TagWbType6:
+            case TagWbType7:
+                return getWbTypeDescription(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getWbTypeDescription(int tagType)
+    {
+        Integer wbtype = _directory.getInteger(tagType);
+        if (wbtype == null)
+            return null;
+
+        return super.getLightSourceDescription(wbtype.shortValue());
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java
new file mode 100644
index 0000000..036c761
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PanasonicRawWbInfoDirectory extends Directory
+{
+    public static final int TagNumWbEntries = 0;
+
+    public static final int TagWbType1 = 1;
+    public static final int TagWbRbLevels1 = 2;
+
+    public static final int TagWbType2 = 4;
+    public static final int TagWbRbLevels2 = 5;
+
+    public static final int TagWbType3 = 7;
+    public static final int TagWbRbLevels3 = 8;
+
+    public static final int TagWbType4 = 10;
+    public static final int TagWbRbLevels4 = 11;
+
+    public static final int TagWbType5 = 13;
+    public static final int TagWbRbLevels5 = 14;
+
+    public static final int TagWbType6 = 16;
+    public static final int TagWbRbLevels6 = 17;
+
+    public static final int TagWbType7 = 19;
+    public static final int TagWbRbLevels7 = 20;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagNumWbEntries, "Num WB Entries");
+        _tagNameMap.put(TagWbType1, "WB Type 1");
+        _tagNameMap.put(TagWbRbLevels1, "WB RGB Levels 1");
+        _tagNameMap.put(TagWbType2, "WB Type 2");
+        _tagNameMap.put(TagWbRbLevels2, "WB RGB Levels 2");
+        _tagNameMap.put(TagWbType3, "WB Type 3");
+        _tagNameMap.put(TagWbRbLevels3, "WB RGB Levels 3");
+        _tagNameMap.put(TagWbType4, "WB Type 4");
+        _tagNameMap.put(TagWbRbLevels4, "WB RGB Levels 4");
+        _tagNameMap.put(TagWbType5, "WB Type 5");
+        _tagNameMap.put(TagWbRbLevels5, "WB RGB Levels 5");
+        _tagNameMap.put(TagWbType6, "WB Type 6");
+        _tagNameMap.put(TagWbRbLevels6, "WB RGB Levels 6");
+        _tagNameMap.put(TagWbType7, "WB Type 7");
+        _tagNameMap.put(TagWbRbLevels7, "WB RGB Levels 7");
+    }
+
+    public PanasonicRawWbInfoDirectory()
+    {
+        this.setDescriptor(new PanasonicRawWbInfoDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PanasonicRaw WbInfo";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PrintIMDescriptor.java b/Source/com/drew/metadata/exif/PrintIMDescriptor.java
new file mode 100644
index 0000000..50400ee
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PrintIMDescriptor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.PrintIMDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link PrintIMDirectory}.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PrintIMDescriptor extends TagDescriptor<PrintIMDirectory>
+{
+    public PrintIMDescriptor(@NotNull PrintIMDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagPrintImVersion:
+                return super.getDescription(tagType);
+            default:
+                Integer value = _directory.getInteger(tagType);
+                if (value == null)
+                    return null;
+                return String.format("0x%08x", value);
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/exif/PrintIMDirectory.java b/Source/com/drew/metadata/exif/PrintIMDirectory.java
new file mode 100644
index 0000000..abada9c
--- /dev/null
+++ b/Source/com/drew/metadata/exif/PrintIMDirectory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags can be found in Epson proprietary metadata. The index values are 'fake' but
+ * chosen specifically to make processing easier
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PrintIMDirectory extends Directory
+{
+    public static final int TagPrintImVersion = 0x0000;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagPrintImVersion, "PrintIM Version");
+    }
+
+    public PrintIMDirectory()
+    {
+        this.setDescriptor(new PrintIMDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PrintIM";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java
new file mode 100644
index 0000000..daabd18
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link AppleMakernoteDirectory}.
+ * <p>
+ * Using information from http://owl.phy.queensu.ca/~phil/exiftool/TagNames/Apple.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class AppleMakernoteDescriptor extends TagDescriptor<AppleMakernoteDirectory>
+{
+    public AppleMakernoteDescriptor(@NotNull AppleMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case AppleMakernoteDirectory.TAG_HDR_IMAGE_TYPE:
+                return getHdrImageTypeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getHdrImageTypeDescription()
+    {
+        return getIndexedDescription(AppleMakernoteDirectory.TAG_HDR_IMAGE_TYPE, 3, "HDR Image", "Original Image");
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java
new file mode 100644
index 0000000..604c73e
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Apple cameras.
+ * <p>
+ * Using information from http://owl.phy.queensu.ca/~phil/exiftool/TagNames/Apple.html
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class AppleMakernoteDirectory extends Directory
+{
+    public static final int TAG_RUN_TIME = 0x0003;
+    public static final int TAG_HDR_IMAGE_TYPE = 0x000a;
+    public static final int TAG_BURST_UUID = 0x000b;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_RUN_TIME, "Run Time");
+        _tagNameMap.put(TAG_HDR_IMAGE_TYPE, "HDR Image Type");
+        _tagNameMap.put(TAG_BURST_UUID, "Burst UUID");
+    }
+
+    public AppleMakernoteDirectory()
+    {
+        this.setDescriptor(new AppleMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Apple Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
index 1f495db..4eb035a 100644
--- a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,6 +24,9 @@ import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import java.text.DecimalFormat;
+import java.util.HashMap;
+
 import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
 
 /**
@@ -31,6 +34,7 @@ import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory>
 {
     public CanonMakernoteDescriptor(@NotNull CanonMakernoteDirectory directory)
@@ -51,6 +55,8 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
                 return getFocusTypeDescription();
             case CameraSettings.TAG_DIGITAL_ZOOM:
                 return getDigitalZoomDescription();
+            case CameraSettings.TAG_RECORD_MODE:
+                return getRecordModeDescription();
             case CameraSettings.TAG_QUALITY:
                 return getQualityDescription();
             case CameraSettings.TAG_MACRO_MODE:
@@ -81,6 +87,8 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
                 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:
@@ -97,6 +105,28 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
                 return getAfPointUsedDescription();
             case FocalLength.TAG_FLASH_BIAS:
                 return getFlashBiasDescription();
+            case AFInfo.TAG_AF_POINTS_IN_FOCUS:
+                return getTagAfPointsInFocus();
+            case CameraSettings.TAG_MAX_APERTURE:
+                return getMaxApertureDescription();
+            case CameraSettings.TAG_MIN_APERTURE:
+                return getMinApertureDescription();
+            case CameraSettings.TAG_FOCUS_CONTINUOUS:
+                return getFocusContinuousDescription();
+            case CameraSettings.TAG_AE_SETTING:
+                return getAESettingDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_DISPLAY_APERTURE:
+                return getDisplayApertureDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_SPOT_METERING_MODE:
+                return getSpotMeteringModeDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_PHOTO_EFFECT:
+                return getPhotoEffectDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_MANUAL_FLASH_OUTPUT:
+                return getManualFlashOutputDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_COLOR_TONE:
+                return getColorToneDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY:
+                return getSRawQualityDescription();
 
             // It turns out that these values are dependent upon the camera model and therefore the below code was
             // incorrect for some Canon models.  This needs to be revisited.
@@ -135,6 +165,7 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
     @Nullable
     public String getSerialNumberDescription()
     {
+        // http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html
         Integer value = _directory.getInteger(TAG_CANON_SERIAL_NUMBER);
         if (value == null)
             return null;
@@ -340,7 +371,7 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
         // not
         //  0, 0.33,  0.5, 0.66,  1
 
-        return ((isNegative) ? "-" : "") + Float.toString(value / 32f) + " EV";
+        return (isNegative ? "-" : "") + Float.toString(value / 32f) + " EV";
     }
 
     @Nullable
@@ -360,6 +391,28 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
         }
     }
 
+    @Nullable
+    public String getTagAfPointsInFocus()
+    {
+        Integer value = _directory.getInteger(AFInfo.TAG_AF_POINTS_IN_FOCUS);
+        if (value == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        for (int i = 0; i < 16; i++)
+        {
+            if ((value & 1 << i) != 0)
+            {
+                if (sb.length() != 0)
+                    sb.append(',');
+                sb.append(i);
+            }
+        }
+
+        return sb.length() == 0 ? "None" : sb.toString();
+    }
+
     @Nullable
     public String getWhiteBalanceDescription()
     {
@@ -387,16 +440,16 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
         Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_DETAILS);
         if (value == null)
             return null;
-        if (((value >> 14) & 1) > 0) {
+        if (((value >> 14) & 1) != 0) {
             return "External E-TTL";
         }
-        if (((value >> 13) & 1) > 0) {
+        if (((value >> 13) & 1) != 0) {
             return "Internal flash";
         }
-        if (((value >> 11) & 1) > 0) {
+        if (((value >> 11) & 1) != 0) {
             return "FP sync used";
         }
-        if (((value >> 4) & 1) > 0) {
+        if (((value >> 4) & 1) != 0) {
             return "FP sync enabled";
         }
         return "Unknown (" + value + ")";
@@ -449,6 +502,39 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
         );
     }
 
+    @Nullable
+    public String getLensTypeDescription() {
+        Integer value = _directory.getInteger(CameraSettings.TAG_LENS_TYPE);
+        if (value == null)
+            return null;
+
+        return _lensTypeById.containsKey(value)
+            ? _lensTypeById.get(value)
+            : String.format("Unknown (%d)", value);
+    }
+
+    @Nullable
+    public String getMaxApertureDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_MAX_APERTURE);
+        if (value == null)
+            return null;
+        if (value > 512)
+            return String.format("Unknown (%d)", value);
+        return getFStopDescription(Math.exp(decodeCanonEv(value) * Math.log(2.0) / 2.0));
+    }
+
+    @Nullable
+    public String getMinApertureDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_MIN_APERTURE);
+        if (value == null)
+            return null;
+        if (value > 512)
+            return String.format("Unknown (%d)", value);
+        return getFStopDescription(Math.exp(decodeCanonEv(value) * Math.log(2.0) / 2.0));
+    }
+
     @Nullable
     public String getAfPointSelectedDescription()
     {
@@ -484,7 +570,7 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
 
         // Canon PowerShot S3 is special
         int canonMask = 0x4000;
-        if ((value & canonMask) > 0)
+        if ((value & canonMask) != 0)
             return "" + (value & ~canonMask);
 
         switch (value) {
@@ -661,8 +747,8 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
         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";
+            DecimalFormat format = new DecimalFormat("0.##");
+            return format.format((double)value * 0.1d) + " sec";
         }
     }
 
@@ -684,6 +770,12 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
         return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "No digital zoom", "2x", "4x");
     }
 
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_RECORD_MODE, 1, "JPEG", "CRW+THM", "AVI+THM", "TIF", "TIF+JPEG", "CR2", "CR2+JPEG", null, "MOV", "MP4");
+    }
+
     @Nullable
     public String getFocusTypeDescription()
     {
@@ -709,4 +801,355 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect
     {
         return getIndexedDescription(CameraSettings.TAG_FLASH_ACTIVITY, "Flash did not fire", "Flash fired");
     }
+
+    @Nullable
+    public String getFocusContinuousDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_FOCUS_CONTINUOUS, 0,
+            "Single", "Continuous", null, null, null, null, null, null, "Manual");
+    }
+
+    @Nullable
+    public String getAESettingDescription()
+    {
+        return getIndexedDescription(CameraSettings.TAG_AE_SETTING, 0,
+            "Normal AE", "Exposure Compensation", "AE Lock", "AE Lock + Exposure Comp.", "No AE");
+    }
+
+    @Nullable
+    public String getDisplayApertureDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_DISPLAY_APERTURE);
+        if (value == null)
+            return null;
+
+        if (value == 0xFFFF)
+            return value.toString();
+        return getFStopDescription(value / 10f);
+    }
+
+    @Nullable
+    public String getSpotMeteringModeDescription()
+    {
+        return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SPOT_METERING_MODE, 0,
+            "Center", "AF Point");
+    }
+
+    @Nullable
+    public String getPhotoEffectDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_PHOTO_EFFECT);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "Off";
+            case 1:
+                return "Vivid";
+            case 2:
+                return "Neutral";
+            case 3:
+                return "Smooth";
+            case 4:
+                return "Sepia";
+            case 5:
+                return "B&W";
+            case 6:
+                return "Custom";
+            case 100:
+                return "My Color Data";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getManualFlashOutputDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_MANUAL_FLASH_OUTPUT);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "n/a";
+            case 0x500:
+                return "Full";
+            case 0x502:
+                return "Medium";
+            case 0x504:
+                return "Low";
+            case 0x7fff:
+                return "n/a";   // (EOS models)
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorToneDescription()
+    {
+        Integer value = _directory.getInteger(CameraSettings.TAG_COLOR_TONE);
+        if (value == null)
+            return null;
+
+        return value == 0x7fff ? "n/a" : value.toString();
+    }
+
+    @Nullable
+    public String getSRawQualityDescription()
+    {
+        return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY, 0, "n/a", "sRAW1 (mRAW)", "sRAW2 (sRAW)");
+    }
+
+    /**
+     * Canon hex-based EV (modulo 0x20) to real number.
+     *
+     * Converted from Exiftool version 10.10 created by Phil Harvey
+     * http://www.sno.phy.queensu.ca/~phil/exiftool/
+     * lib\Image\ExifTool\Canon.pm
+     *
+     *         eg) 0x00 -> 0
+     *             0x0c -> 0.33333
+     *             0x10 -> 0.5
+     *             0x14 -> 0.66666
+     *             0x20 -> 1   ... etc
+     */
+    private double decodeCanonEv(int val)
+    {
+        int sign = 1;
+        if (val < 0)
+        {
+            val = -val;
+            sign = -1;
+        }
+
+        int frac = val & 0x1f;
+        val -= frac;
+
+        if (frac == 0x0c)
+            frac = 0x20 / 3;
+        else if (frac == 0x14)
+            frac = 0x40 / 3;
+
+        return sign * (val + frac) / (double)0x20;
+    }
+
+    /**
+     *  Map from <see cref="CanonMakernoteDirectory.CameraSettings.TagLensType"/> to string descriptions.
+     *
+     *  Data sourced from http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html#LensType
+     *
+     *  Note that only Canon lenses are listed. Lenses from other manufacturers may identify themselves to the camera
+     *  as being from this set, but in fact may be quite different. This limits the usefulness of this data,
+     *  unfortunately.
+     */
+    private static final HashMap<Integer, String> _lensTypeById = new HashMap<Integer, String>();
+
+    static {
+        _lensTypeById.put(1, "Canon EF 50mm f/1.8");
+        _lensTypeById.put(2, "Canon EF 28mm f/2.8");
+        _lensTypeById.put(3, "Canon EF 135mm f/2.8 Soft");
+        _lensTypeById.put(4, "Canon EF 35-105mm f/3.5-4.5 or Sigma Lens");
+        _lensTypeById.put(5, "Canon EF 35-70mm f/3.5-4.5");
+        _lensTypeById.put(6, "Canon EF 28-70mm f/3.5-4.5 or Sigma or Tokina Lens");
+        _lensTypeById.put(7, "Canon EF 100-300mm f/5.6L");
+        _lensTypeById.put(8, "Canon EF 100-300mm f/5.6 or Sigma or Tokina Lens");
+        _lensTypeById.put(9, "Canon EF 70-210mm f/4");
+        _lensTypeById.put(10, "Canon EF 50mm f/2.5 Macro or Sigma Lens");
+        _lensTypeById.put(11, "Canon EF 35mm f/2");
+        _lensTypeById.put(13, "Canon EF 15mm f/2.8 Fisheye");
+        _lensTypeById.put(14, "Canon EF 50-200mm f/3.5-4.5L");
+        _lensTypeById.put(15, "Canon EF 50-200mm f/3.5-4.5");
+        _lensTypeById.put(16, "Canon EF 35-135mm f/3.5-4.5");
+        _lensTypeById.put(17, "Canon EF 35-70mm f/3.5-4.5A");
+        _lensTypeById.put(18, "Canon EF 28-70mm f/3.5-4.5");
+        _lensTypeById.put(20, "Canon EF 100-200mm f/4.5A");
+        _lensTypeById.put(21, "Canon EF 80-200mm f/2.8L");
+        _lensTypeById.put(22, "Canon EF 20-35mm f/2.8L or Tokina Lens");
+        _lensTypeById.put(23, "Canon EF 35-105mm f/3.5-4.5");
+        _lensTypeById.put(24, "Canon EF 35-80mm f/4-5.6 Power Zoom");
+        _lensTypeById.put(25, "Canon EF 35-80mm f/4-5.6 Power Zoom");
+        _lensTypeById.put(26, "Canon EF 100mm f/2.8 Macro or Other Lens");
+        _lensTypeById.put(27, "Canon EF 35-80mm f/4-5.6");
+        _lensTypeById.put(28, "Canon EF 80-200mm f/4.5-5.6 or Tamron Lens");
+        _lensTypeById.put(29, "Canon EF 50mm f/1.8 II");
+        _lensTypeById.put(30, "Canon EF 35-105mm f/4.5-5.6");
+        _lensTypeById.put(31, "Canon EF 75-300mm f/4-5.6 or Tamron Lens");
+        _lensTypeById.put(32, "Canon EF 24mm f/2.8 or Sigma Lens");
+        _lensTypeById.put(33, "Voigtlander or Carl Zeiss Lens");
+        _lensTypeById.put(35, "Canon EF 35-80mm f/4-5.6");
+        _lensTypeById.put(36, "Canon EF 38-76mm f/4.5-5.6");
+        _lensTypeById.put(37, "Canon EF 35-80mm f/4-5.6 or Tamron Lens");
+        _lensTypeById.put(38, "Canon EF 80-200mm f/4.5-5.6");
+        _lensTypeById.put(39, "Canon EF 75-300mm f/4-5.6");
+        _lensTypeById.put(40, "Canon EF 28-80mm f/3.5-5.6");
+        _lensTypeById.put(41, "Canon EF 28-90mm f/4-5.6");
+        _lensTypeById.put(42, "Canon EF 28-200mm f/3.5-5.6 or Tamron Lens");
+        _lensTypeById.put(43, "Canon EF 28-105mm f/4-5.6");
+        _lensTypeById.put(44, "Canon EF 90-300mm f/4.5-5.6");
+        _lensTypeById.put(45, "Canon EF-S 18-55mm f/3.5-5.6 [II]");
+        _lensTypeById.put(46, "Canon EF 28-90mm f/4-5.6");
+        _lensTypeById.put(47, "Zeiss Milvus 35mm f/2 or 50mm f/2");
+        _lensTypeById.put(48, "Canon EF-S 18-55mm f/3.5-5.6 IS");
+        _lensTypeById.put(49, "Canon EF-S 55-250mm f/4-5.6 IS");
+        _lensTypeById.put(50, "Canon EF-S 18-200mm f/3.5-5.6 IS");
+        _lensTypeById.put(51, "Canon EF-S 18-135mm f/3.5-5.6 IS");
+        _lensTypeById.put(52, "Canon EF-S 18-55mm f/3.5-5.6 IS II");
+        _lensTypeById.put(53, "Canon EF-S 18-55mm f/3.5-5.6 III");
+        _lensTypeById.put(54, "Canon EF-S 55-250mm f/4-5.6 IS II");
+        _lensTypeById.put(94, "Canon TS-E 17mm f/4L");
+        _lensTypeById.put(95, "Canon TS-E 24.0mm f/3.5 L II");
+        _lensTypeById.put(124, "Canon MP-E 65mm f/2.8 1-5x Macro Photo");
+        _lensTypeById.put(125, "Canon TS-E 24mm f/3.5L");
+        _lensTypeById.put(126, "Canon TS-E 45mm f/2.8");
+        _lensTypeById.put(127, "Canon TS-E 90mm f/2.8");
+        _lensTypeById.put(129, "Canon EF 300mm f/2.8L");
+        _lensTypeById.put(130, "Canon EF 50mm f/1.0L");
+        _lensTypeById.put(131, "Canon EF 28-80mm f/2.8-4L or Sigma Lens");
+        _lensTypeById.put(132, "Canon EF 1200mm f/5.6L");
+        _lensTypeById.put(134, "Canon EF 600mm f/4L IS");
+        _lensTypeById.put(135, "Canon EF 200mm f/1.8L");
+        _lensTypeById.put(136, "Canon EF 300mm f/2.8L");
+        _lensTypeById.put(137, "Canon EF 85mm f/1.2L or Sigma or Tamron Lens");
+        _lensTypeById.put(138, "Canon EF 28-80mm f/2.8-4L");
+        _lensTypeById.put(139, "Canon EF 400mm f/2.8L");
+        _lensTypeById.put(140, "Canon EF 500mm f/4.5L");
+        _lensTypeById.put(141, "Canon EF 500mm f/4.5L");
+        _lensTypeById.put(142, "Canon EF 300mm f/2.8L IS");
+        _lensTypeById.put(143, "Canon EF 500mm f/4L IS or Sigma Lens");
+        _lensTypeById.put(144, "Canon EF 35-135mm f/4-5.6 USM");
+        _lensTypeById.put(145, "Canon EF 100-300mm f/4.5-5.6 USM");
+        _lensTypeById.put(146, "Canon EF 70-210mm f/3.5-4.5 USM");
+        _lensTypeById.put(147, "Canon EF 35-135mm f/4-5.6 USM");
+        _lensTypeById.put(148, "Canon EF 28-80mm f/3.5-5.6 USM");
+        _lensTypeById.put(149, "Canon EF 100mm f/2 USM");
+        _lensTypeById.put(150, "Canon EF 14mm f/2.8L or Sigma Lens");
+        _lensTypeById.put(151, "Canon EF 200mm f/2.8L");
+        _lensTypeById.put(152, "Canon EF 300mm f/4L IS or Sigma Lens");
+        _lensTypeById.put(153, "Canon EF 35-350mm f/3.5-5.6L or Sigma or Tamron Lens");
+        _lensTypeById.put(154, "Canon EF 20mm f/2.8 USM or Zeiss Lens");
+        _lensTypeById.put(155, "Canon EF 85mm f/1.8 USM");
+        _lensTypeById.put(156, "Canon EF 28-105mm f/3.5-4.5 USM or Tamron Lens");
+        _lensTypeById.put(160, "Canon EF 20-35mm f/3.5-4.5 USM or Tamron or Tokina Lens");
+        _lensTypeById.put(161, "Canon EF 28-70mm f/2.8L or Sigma or Tamron Lens");
+        _lensTypeById.put(162, "Canon EF 200mm f/2.8L");
+        _lensTypeById.put(163, "Canon EF 300mm f/4L");
+        _lensTypeById.put(164, "Canon EF 400mm f/5.6L");
+        _lensTypeById.put(165, "Canon EF 70-200mm f/2.8 L");
+        _lensTypeById.put(166, "Canon EF 70-200mm f/2.8 L + 1.4x");
+        _lensTypeById.put(167, "Canon EF 70-200mm f/2.8 L + 2x");
+        _lensTypeById.put(168, "Canon EF 28mm f/1.8 USM or Sigma Lens");
+        _lensTypeById.put(169, "Canon EF 17-35mm f/2.8L or Sigma Lens");
+        _lensTypeById.put(170, "Canon EF 200mm f/2.8L II");
+        _lensTypeById.put(171, "Canon EF 300mm f/4L");
+        _lensTypeById.put(172, "Canon EF 400mm f/5.6L or Sigma Lens");
+        _lensTypeById.put(173, "Canon EF 180mm Macro f/3.5L or Sigma Lens");
+        _lensTypeById.put(174, "Canon EF 135mm f/2L or Other Lens");
+        _lensTypeById.put(175, "Canon EF 400mm f/2.8L");
+        _lensTypeById.put(176, "Canon EF 24-85mm f/3.5-4.5 USM");
+        _lensTypeById.put(177, "Canon EF 300mm f/4L IS");
+        _lensTypeById.put(178, "Canon EF 28-135mm f/3.5-5.6 IS");
+        _lensTypeById.put(179, "Canon EF 24mm f/1.4L");
+        _lensTypeById.put(180, "Canon EF 35mm f/1.4L or Other Lens");
+        _lensTypeById.put(181, "Canon EF 100-400mm f/4.5-5.6L IS + 1.4x or Sigma Lens");
+        _lensTypeById.put(182, "Canon EF 100-400mm f/4.5-5.6L IS + 2x or Sigma Lens");
+        _lensTypeById.put(183, "Canon EF 100-400mm f/4.5-5.6L IS or Sigma Lens");
+        _lensTypeById.put(184, "Canon EF 400mm f/2.8L + 2x");
+        _lensTypeById.put(185, "Canon EF 600mm f/4L IS");
+        _lensTypeById.put(186, "Canon EF 70-200mm f/4L");
+        _lensTypeById.put(187, "Canon EF 70-200mm f/4L + 1.4x");
+        _lensTypeById.put(188, "Canon EF 70-200mm f/4L + 2x");
+        _lensTypeById.put(189, "Canon EF 70-200mm f/4L + 2.8x");
+        _lensTypeById.put(190, "Canon EF 100mm f/2.8 Macro USM");
+        _lensTypeById.put(191, "Canon EF 400mm f/4 DO IS");
+        _lensTypeById.put(193, "Canon EF 35-80mm f/4-5.6 USM");
+        _lensTypeById.put(194, "Canon EF 80-200mm f/4.5-5.6 USM");
+        _lensTypeById.put(195, "Canon EF 35-105mm f/4.5-5.6 USM");
+        _lensTypeById.put(196, "Canon EF 75-300mm f/4-5.6 USM");
+        _lensTypeById.put(197, "Canon EF 75-300mm f/4-5.6 IS USM");
+        _lensTypeById.put(198, "Canon EF 50mm f/1.4 USM or Zeiss Lens");
+        _lensTypeById.put(199, "Canon EF 28-80mm f/3.5-5.6 USM");
+        _lensTypeById.put(200, "Canon EF 75-300mm f/4-5.6 USM");
+        _lensTypeById.put(201, "Canon EF 28-80mm f/3.5-5.6 USM");
+        _lensTypeById.put(202, "Canon EF 28-80mm f/3.5-5.6 USM IV");
+        _lensTypeById.put(208, "Canon EF 22-55mm f/4-5.6 USM");
+        _lensTypeById.put(209, "Canon EF 55-200mm f/4.5-5.6");
+        _lensTypeById.put(210, "Canon EF 28-90mm f/4-5.6 USM");
+        _lensTypeById.put(211, "Canon EF 28-200mm f/3.5-5.6 USM");
+        _lensTypeById.put(212, "Canon EF 28-105mm f/4-5.6 USM");
+        _lensTypeById.put(213, "Canon EF 90-300mm f/4.5-5.6 USM or Tamron Lens");
+        _lensTypeById.put(214, "Canon EF-S 18-55mm f/3.5-5.6 USM");
+        _lensTypeById.put(215, "Canon EF 55-200mm f/4.5-5.6 II USM");
+        _lensTypeById.put(217, "Tamron AF 18-270mm f/3.5-6.3 Di II VC PZD");
+        _lensTypeById.put(224, "Canon EF 70-200mm f/2.8L IS");
+        _lensTypeById.put(225, "Canon EF 70-200mm f/2.8L IS + 1.4x");
+        _lensTypeById.put(226, "Canon EF 70-200mm f/2.8L IS + 2x");
+        _lensTypeById.put(227, "Canon EF 70-200mm f/2.8L IS + 2.8x");
+        _lensTypeById.put(228, "Canon EF 28-105mm f/3.5-4.5 USM");
+        _lensTypeById.put(229, "Canon EF 16-35mm f/2.8L");
+        _lensTypeById.put(230, "Canon EF 24-70mm f/2.8L");
+        _lensTypeById.put(231, "Canon EF 17-40mm f/4L");
+        _lensTypeById.put(232, "Canon EF 70-300mm f/4.5-5.6 DO IS USM");
+        _lensTypeById.put(233, "Canon EF 28-300mm f/3.5-5.6L IS");
+        _lensTypeById.put(234, "Canon EF-S 17-85mm f/4-5.6 IS USM or Tokina Lens");
+        _lensTypeById.put(235, "Canon EF-S 10-22mm f/3.5-4.5 USM");
+        _lensTypeById.put(236, "Canon EF-S 60mm f/2.8 Macro USM");
+        _lensTypeById.put(237, "Canon EF 24-105mm f/4L IS");
+        _lensTypeById.put(238, "Canon EF 70-300mm f/4-5.6 IS USM");
+        _lensTypeById.put(239, "Canon EF 85mm f/1.2L II");
+        _lensTypeById.put(240, "Canon EF-S 17-55mm f/2.8 IS USM");
+        _lensTypeById.put(241, "Canon EF 50mm f/1.2L");
+        _lensTypeById.put(242, "Canon EF 70-200mm f/4L IS");
+        _lensTypeById.put(243, "Canon EF 70-200mm f/4L IS + 1.4x");
+        _lensTypeById.put(244, "Canon EF 70-200mm f/4L IS + 2x");
+        _lensTypeById.put(245, "Canon EF 70-200mm f/4L IS + 2.8x");
+        _lensTypeById.put(246, "Canon EF 16-35mm f/2.8L II");
+        _lensTypeById.put(247, "Canon EF 14mm f/2.8L II USM");
+        _lensTypeById.put(248, "Canon EF 200mm f/2L IS or Sigma Lens");
+        _lensTypeById.put(249, "Canon EF 800mm f/5.6L IS");
+        _lensTypeById.put(250, "Canon EF 24mm f/1.4L II or Sigma Lens");
+        _lensTypeById.put(251, "Canon EF 70-200mm f/2.8L IS II USM");
+        _lensTypeById.put(252, "Canon EF 70-200mm f/2.8L IS II USM + 1.4x");
+        _lensTypeById.put(253, "Canon EF 70-200mm f/2.8L IS II USM + 2x");
+        _lensTypeById.put(254, "Canon EF 100mm f/2.8L Macro IS USM");
+        _lensTypeById.put(255, "Sigma 24-105mm f/4 DG OS HSM | A or Other Sigma Lens");
+        _lensTypeById.put(488, "Canon EF-S 15-85mm f/3.5-5.6 IS USM");
+        _lensTypeById.put(489, "Canon EF 70-300mm f/4-5.6L IS USM");
+        _lensTypeById.put(490, "Canon EF 8-15mm f/4L Fisheye USM");
+        _lensTypeById.put(491, "Canon EF 300mm f/2.8L IS II USM");
+        _lensTypeById.put(492, "Canon EF 400mm f/2.8L IS II USM");
+        _lensTypeById.put(493, "Canon EF 500mm f/4L IS II USM or EF 24-105mm f4L IS USM");
+        _lensTypeById.put(494, "Canon EF 600mm f/4.0L IS II USM");
+        _lensTypeById.put(495, "Canon EF 24-70mm f/2.8L II USM");
+        _lensTypeById.put(496, "Canon EF 200-400mm f/4L IS USM");
+        _lensTypeById.put(499, "Canon EF 200-400mm f/4L IS USM + 1.4x");
+        _lensTypeById.put(502, "Canon EF 28mm f/2.8 IS USM");
+        _lensTypeById.put(503, "Canon EF 24mm f/2.8 IS USM");
+        _lensTypeById.put(504, "Canon EF 24-70mm f/4L IS USM");
+        _lensTypeById.put(505, "Canon EF 35mm f/2 IS USM");
+        _lensTypeById.put(506, "Canon EF 400mm f/4 DO IS II USM");
+        _lensTypeById.put(507, "Canon EF 16-35mm f/4L IS USM");
+        _lensTypeById.put(508, "Canon EF 11-24mm f/4L USM");
+        _lensTypeById.put(747, "Canon EF 100-400mm f/4.5-5.6L IS II USM");
+        _lensTypeById.put(750, "Canon EF 35mm f/1.4L II USM");
+        _lensTypeById.put(4142, "Canon EF-S 18-135mm f/3.5-5.6 IS STM");
+        _lensTypeById.put(4143, "Canon EF-M 18-55mm f/3.5-5.6 IS STM or Tamron Lens");
+        _lensTypeById.put(4144, "Canon EF 40mm f/2.8 STM");
+        _lensTypeById.put(4145, "Canon EF-M 22mm f/2 STM");
+        _lensTypeById.put(4146, "Canon EF-S 18-55mm f/3.5-5.6 IS STM");
+        _lensTypeById.put(4147, "Canon EF-M 11-22mm f/4-5.6 IS STM");
+        _lensTypeById.put(4148, "Canon EF-S 55-250mm f/4-5.6 IS STM");
+        _lensTypeById.put(4149, "Canon EF-M 55-200mm f/4.5-6.3 IS STM");
+        _lensTypeById.put(4150, "Canon EF-S 10-18mm f/4.5-5.6 IS STM");
+        _lensTypeById.put(4152, "Canon EF 24-105mm f/3.5-5.6 IS STM");
+        _lensTypeById.put(4153, "Canon EF-M 15-45mm f/3.5-6.3 IS STM");
+        _lensTypeById.put(4154, "Canon EF-S 24mm f/2.8 STM");
+        _lensTypeById.put(4156, "Canon EF 50mm f/1.8 STM");
+        _lensTypeById.put(36912, "Canon EF-S 18-135mm f/3.5-5.6 IS USM");
+        _lensTypeById.put(65535, "N/A");
+    }
 }
diff --git a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
index 72da6b6..cfe9c50 100644
--- a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CanonMakernoteDirectory extends Directory
 {
     // These TAG_*_ARRAY Exif tags map to arrays of int16 values which are split out into separate 'fake' tags.
@@ -158,7 +159,7 @@ public class CanonMakernoteDirectory extends Directory
          */
         public static final int TAG_FOCUS_MODE_1 = OFFSET + 0x07;
         public static final int TAG_UNKNOWN_3 = OFFSET + 0x08;
-        public static final int TAG_UNKNOWN_4 = OFFSET + 0x09;
+        public static final int TAG_RECORD_MODE = OFFSET + 0x09;
         /**
          * 0 = Large
          * 1 = Medium
@@ -244,25 +245,36 @@ public class CanonMakernoteDirectory extends Directory
          */
         public static final int TAG_EXPOSURE_MODE = OFFSET + 0x14;
         public static final int TAG_UNKNOWN_7 = OFFSET + 0x15;
-        public static final int TAG_UNKNOWN_8 = OFFSET + 0x16;
+        public static final int TAG_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;
+        public static final int TAG_MAX_APERTURE = OFFSET + 0x1A;
+        public static final int TAG_MIN_APERTURE = 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;
+        public static final int TAG_FOCUS_CONTINUOUS = OFFSET + 0x1E;
+        public static final int TAG_AE_SETTING = OFFSET + 0x1F;
         /**
          * 0 = Focus Mode: Single
          * 1 = Focus Mode: Continuous
          */
         public static final int TAG_FOCUS_MODE_2 = OFFSET + 0x20;
+
+        public static final int TAG_DISPLAY_APERTURE = OFFSET + 0x21;
+        public static final int TAG_ZOOM_SOURCE_WIDTH = OFFSET + 0x22;
+        public static final int TAG_ZOOM_TARGET_WIDTH = OFFSET + 0x23;
+
+        public static final int TAG_SPOT_METERING_MODE = OFFSET + 0x25;
+        public static final int TAG_PHOTO_EFFECT = OFFSET + 0x26;
+        public static final int TAG_MANUAL_FLASH_OUTPUT = OFFSET + 0x27;
+
+        public static final int TAG_COLOR_TONE = OFFSET + 0x29;
+        public static final int TAG_SRAW_QUALITY = OFFSET + 0x2D;
     }
 
     public final static class FocalLength
@@ -514,16 +526,24 @@ public class CanonMakernoteDirectory extends Directory
         _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_RECORD_MODE, "Record Mode");
         _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom");
         _tagNameMap.put(CameraSettings.TAG_FOCUS_TYPE, "Focus Type");
         _tagNameMap.put(CameraSettings.TAG_UNKNOWN_7, "Unknown Camera Setting 7");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_8, "Unknown Camera Setting 8");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_9, "Unknown Camera Setting 9");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_10, "Unknown Camera Setting 10");
+        _tagNameMap.put(CameraSettings.TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(CameraSettings.TAG_MAX_APERTURE, "Max Aperture");
+        _tagNameMap.put(CameraSettings.TAG_MIN_APERTURE, "Min Aperture");
         _tagNameMap.put(CameraSettings.TAG_FLASH_ACTIVITY, "Flash Activity");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_12, "Unknown Camera Setting 12");
-        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_13, "Unknown Camera Setting 13");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_CONTINUOUS, "Focus Continuous");
+        _tagNameMap.put(CameraSettings.TAG_AE_SETTING, "AE Setting");
+        _tagNameMap.put(CameraSettings.TAG_DISPLAY_APERTURE, "Display Aperture");
+        _tagNameMap.put(CameraSettings.TAG_ZOOM_SOURCE_WIDTH, "Zoom Source Width");
+        _tagNameMap.put(CameraSettings.TAG_ZOOM_TARGET_WIDTH, "Zoom Target Width");
+        _tagNameMap.put(CameraSettings.TAG_SPOT_METERING_MODE, "Spot Metering Mode");
+        _tagNameMap.put(CameraSettings.TAG_PHOTO_EFFECT, "Photo Effect");
+        _tagNameMap.put(CameraSettings.TAG_MANUAL_FLASH_OUTPUT, "Manual Flash Output");
+        _tagNameMap.put(CameraSettings.TAG_COLOR_TONE, "Color Tone");
+        _tagNameMap.put(CameraSettings.TAG_SRAW_QUALITY, "SRAW Quality");
 
         _tagNameMap.put(FocalLength.TAG_WHITE_BALANCE, "White Balance");
         _tagNameMap.put(FocalLength.TAG_SEQUENCE_NUMBER, "Sequence Number");
@@ -575,7 +595,7 @@ public class CanonMakernoteDirectory extends Directory
         _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_AF_POINTS_IN_FOCUS, "AF Points in Focus");
         _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_1, "Primary AF Point 1");
         _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_2, "Primary AF Point 2");
 
@@ -671,6 +691,12 @@ public class CanonMakernoteDirectory extends Directory
     {
         // TODO is there some way to drop out 'null' or 'zero' values that are present in the array to reduce the noise?
 
+        if (!(array instanceof int[])) {
+            // no special handling...
+            super.setObjectArray(tagType, array);
+            return;
+        }
+
         // Certain Canon tags contain arrays of values that we split into 'fake' tags as each
         // index in the array has its own meaning and decoding.
         // Pick those tags out here and throw away the original array.
@@ -708,9 +734,59 @@ public class CanonMakernoteDirectory extends Directory
 //                    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]);
+                // Notes from Exiftool 10.10 by Phil Harvey, lib\Image\Exiftool\Canon.pm:
+                // Auto-focus information used by many older Canon models. The values in this
+                // record are sequential, and some have variable sizes based on the value of
+                // numafpoints (which may be 1,5,7,9,15,45, or 53). The AFArea coordinates are
+                // given in a system where the image has dimensions given by AFImageWidth and
+                // AFImageHeight, and 0,0 is the image center. The direction of the Y axis
+                // depends on the camera model, with positive Y upwards for EOS models, but
+                // apparently downwards for PowerShot models.
+
+                // AFInfo is another array with 'fake' tags. The first int of the array contains
+                // the number of AF points. Iterate through the array one byte at a time, generally
+                // assuming one byte corresponds to one tag UNLESS certain tag numbers are encountered.
+                // For these, read specific subsequent bytes from the array based on the tag type. The
+                // number of bytes read can vary.
+
+                int[] values = (int[])array;
+                int numafpoints = values[0];
+                int tagnumber = 0;
+                for (int i = 0; i < values.length; i++)
+                {
+                    // These two tags store 'numafpoints' bytes of data in the array
+                    if (AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_AREA_X_POSITIONS ||
+                        AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_AREA_Y_POSITIONS)
+                    {
+                        // There could be incorrect data in the array, so boundary check
+                        if (values.length - 1 >= (i + numafpoints))
+                        {
+                            short[] areaPositions = new short[numafpoints];
+                            for (int j = 0; j < areaPositions.length; j++)
+                                areaPositions[j] = (short)values[i + j];
+
+                            super.setObjectArray(AFInfo.OFFSET + tagnumber, areaPositions);
+                        }
+                        i += numafpoints - 1;   // assume these bytes are processed and skip
+                    }
+                    else if (AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_POINTS_IN_FOCUS)
+                    {
+                        short[] pointsInFocus = new short[((numafpoints + 15) / 16)];
+
+                        // There could be incorrect data in the array, so boundary check
+                        if (values.length - 1 >= (i + pointsInFocus.length))
+                        {
+                            for (int j = 0; j < pointsInFocus.length; j++)
+                                pointsInFocus[j] = (short)values[i + j];
+
+                            super.setObjectArray(AFInfo.OFFSET + tagnumber, pointsInFocus);
+                        }
+                        i += pointsInFocus.length - 1;  // assume these bytes are processed and skip
+                    }
+                    else
+                        super.setObjectArray(AFInfo.OFFSET + tagnumber, values[i]);
+                    tagnumber++;
+                }
                 break;
             }
             default: {
diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
index 148dd79..5207cef 100644
--- a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import static com.drew.metadata.exif.makernotes.CasioType1MakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory>
 {
     public CasioType1MakernoteDescriptor(@NotNull CasioType1MakernoteDirectory directory)
@@ -154,11 +155,7 @@ public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1Maker
     public String getObjectDistanceDescription()
     {
         Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE);
-
-        if (value == null)
-            return null;
-
-        return value + " mm";
+        return value == null ? null : getFocalLengthDescription(value);
     }
 
     @Nullable
diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
index 2c4fe85..04fcaad 100644
--- a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType1MakernoteDirectory extends Directory
 {
     public static final int TAG_RECORDING_MODE = 0x0001;
diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
index 4a83f0b..3cf2d1b 100644
--- a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import static com.drew.metadata.exif.makernotes.CasioType2MakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory>
 {
     public CasioType2MakernoteDescriptor(@NotNull CasioType2MakernoteDirectory directory)
@@ -67,8 +68,6 @@ public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2Maker
                 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:
@@ -210,13 +209,6 @@ public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2Maker
         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()
     {
@@ -239,9 +231,7 @@ public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2Maker
     public String getFocalLengthDescription()
     {
         Double value = _directory.getDoubleObject(TAG_FOCAL_LENGTH);
-        if (value == null)
-            return null;
-        return Double.toString(value / 10d) + " mm";
+        return value == null ? null : getFocalLengthDescription(value / 10d);
     }
 
     @Nullable
diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
index 9648e33..55b273e 100644
--- a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class CasioType2MakernoteDirectory extends Directory
 {
     /**
diff --git a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
index 7ae3bbb..e033998 100644
--- a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -48,6 +48,7 @@ import static com.drew.metadata.exif.makernotes.FujifilmMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class FujifilmMakernoteDescriptor extends TagDescriptor<FujifilmMakernoteDirectory>
 {
     public FujifilmMakernoteDescriptor(@NotNull FujifilmMakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
index a5aa44e..047bbdb 100644
--- a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class FujifilmMakernoteDirectory extends Directory
 {
     public static final int TAG_MAKERNOTE_VERSION = 0x0000;
diff --git a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
index 8f2c1aa..eedebe1 100644
--- a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import static com.drew.metadata.exif.makernotes.KodakMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory>
 {
     public KodakMakernoteDescriptor(@NotNull KodakMakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
index b60edc0..dbf9308 100644
--- a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KodakMakernoteDirectory extends Directory
 {
     public final static int TAG_KODAK_MODEL = 0;
diff --git a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
index c5a2935..65b0b80 100644
--- a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ import static com.drew.metadata.exif.makernotes.KyoceraMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDirectory>
 {
     public KyoceraMakernoteDescriptor(@NotNull KyoceraMakernoteDirectory directory)
@@ -50,8 +51,6 @@ public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDi
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case TAG_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
             case TAG_PROPRIETARY_THUMBNAIL:
                 return getProprietaryThumbnailDataDescription();
             default:
@@ -59,12 +58,6 @@ public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDi
         }
     }
 
-    @Nullable
-    public String getPrintImageMatchingInfoDescription()
-    {
-        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
-    }
-
     @Nullable
     public String getProprietaryThumbnailDataDescription()
     {
diff --git a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
index 7139f9c..f67c0a7 100644
--- a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class KyoceraMakernoteDirectory extends Directory
 {
     public static final int TAG_PROPRIETARY_THUMBNAIL = 0x0001;
diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
index 1fb494c..af70827 100644
--- a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import static com.drew.metadata.exif.makernotes.LeicaMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class LeicaMakernoteDescriptor extends TagDescriptor<LeicaMakernoteDirectory>
 {
     public LeicaMakernoteDescriptor(@NotNull LeicaMakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
index a2e6c38..3d23dc2 100644
--- a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class LeicaMakernoteDirectory extends Directory
 {
     public static final int TAG_QUALITY = 0x0300;
diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java
new file mode 100644
index 0000000..3096cb1
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.LeicaType5MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link LeicaType5MakernoteDirectory}.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class LeicaType5MakernoteDescriptor extends TagDescriptor<LeicaType5MakernoteDirectory>
+{
+    public LeicaType5MakernoteDescriptor(@NotNull LeicaType5MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagExposureMode:
+                return getExposureModeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        byte[] values = _directory.getByteArray(TagExposureMode);
+        if (values == null || values.length < 4)
+            return null;
+
+        String join = String.format("%d %d %d %d", values[0], values[1], values[2], values[3]);
+
+        if(join.equals("0 0 0 0"))
+            return "Program AE";
+        else if(join.equals("1 0 0 0"))
+            return "Aperture-priority AE";
+        else if(join.equals("1 1 0 0"))
+            return "Aperture-priority AE (1)";
+        else if(join.equals("2 0 0 0"))
+            return "Shutter speed priority AE";  // guess
+        else if(join.equals("3 0 0 0"))
+            return "Manual";
+        else
+            return String.format("Unknown (%s)", join);
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java
new file mode 100644
index 0000000..ad8492e
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to certain Leica cameras.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class LeicaType5MakernoteDirectory extends Directory
+{
+    public static final int TagLensModel = 0x0303;
+    public static final int TagOriginalFileName = 0x0407;
+    public static final int TagOriginalDirectory = 0x0408;
+    public static final int TagExposureMode = 0x040d;
+    public static final int TagShotInfo = 0x0410;
+    public static final int TagFilmMode = 0x0412;
+    public static final int TagWbRgbLevels = 0x0413;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagLensModel, "Lens Model");
+        _tagNameMap.put(TagOriginalFileName, "Original File Name");
+        _tagNameMap.put(TagOriginalDirectory, "Original Directory");
+        _tagNameMap.put(TagExposureMode, "Exposure Mode");
+        _tagNameMap.put(TagShotInfo, "Shot Info" );
+        _tagNameMap.put(TagFilmMode, "Film Mode");
+        _tagNameMap.put(TagWbRgbLevels, "WB RGB Levels");
+    }
+
+    public LeicaType5MakernoteDirectory()
+    {
+        this.setDescriptor(new LeicaType5MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Leica Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
index 3df1370..0825052 100644
--- a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@ import static com.drew.metadata.exif.makernotes.NikonType1MakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType1MakernoteDescriptor extends TagDescriptor<NikonType1MakernoteDirectory>
 {
     public NikonType1MakernoteDescriptor(@NotNull NikonType1MakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
index ad7ac57..53bf7e6 100644
--- a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType1MakernoteDirectory extends Directory
 {
     public static final int TAG_UNKNOWN_1 = 0x0002;
diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
index a55a911..7f1bf46 100644
--- a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import static com.drew.metadata.exif.makernotes.NikonType2MakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2MakernoteDirectory>
 {
     public NikonType2MakernoteDescriptor(@NotNull NikonType2MakernoteDirectory directory)
@@ -305,7 +306,7 @@ public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2Maker
     private String getEVDescription(int tagType)
     {
         int[] values = _directory.getIntArray(tagType);
-        if (values == null)
+        if (values == null || values.length < 2)
             return null;
         if (values.length < 3 || values[2] == 0)
             return null;
@@ -328,14 +329,7 @@ public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2Maker
     @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());
-
+        return getLensSpecificationDescription(TAG_LENS);
     }
 
     @Nullable
diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
index 39249a1..3ffb795 100644
--- a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -44,6 +44,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class NikonType2MakernoteDirectory extends Directory
 {
     /**
@@ -759,7 +760,7 @@ public class NikonType2MakernoteDirectory extends Directory
     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;
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
     /**
      * Data about changes set by Nikon Capture Editor.
@@ -892,7 +893,7 @@ public class NikonType2MakernoteDirectory extends Directory
         _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_PRINT_IMAGE_MATCHING_INFO, "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");
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java
new file mode 100644
index 0000000..c674c6a
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java
@@ -0,0 +1,1412 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DecimalFormat;
+import java.util.HashMap;
+
+import static com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusCameraSettingsMakernoteDirectory}.
+ * <p>
+ * Some Description functions and the Extender and Lens types lists converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusCameraSettingsMakernoteDescriptor extends TagDescriptor<OlympusCameraSettingsMakernoteDirectory>
+{
+    public OlympusCameraSettingsMakernoteDescriptor(@NotNull OlympusCameraSettingsMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagCameraSettingsVersion:
+                return getCameraSettingsVersionDescription();
+            case TagPreviewImageValid:
+                return getPreviewImageValidDescription();
+
+            case TagExposureMode:
+                return getExposureModeDescription();
+            case TagAeLock:
+                return getAeLockDescription();
+            case TagMeteringMode:
+                return getMeteringModeDescription();
+            case TagExposureShift:
+                return getExposureShiftDescription();
+            case TagNdFilter:
+                return getNdFilterDescription();
+
+            case TagMacroMode:
+                return getMacroModeDescription();
+            case TagFocusMode:
+                return getFocusModeDescription();
+            case TagFocusProcess:
+                return getFocusProcessDescription();
+            case TagAfSearch:
+                return getAfSearchDescription();
+            case TagAfAreas:
+                return getAfAreasDescription();
+            case TagAfPointSelected:
+                return getAfPointSelectedDescription();
+            case TagAfFineTune:
+                return getAfFineTuneDescription();
+
+            case TagFlashMode:
+                return getFlashModeDescription();
+            case TagFlashRemoteControl:
+                return getFlashRemoteControlDescription();
+            case TagFlashControlMode:
+                return getFlashControlModeDescription();
+            case TagFlashIntensity:
+                return getFlashIntensityDescription();
+            case TagManualFlashStrength:
+                return getManualFlashStrengthDescription();
+
+            case TagWhiteBalance2:
+                return getWhiteBalance2Description();
+            case TagWhiteBalanceTemperature:
+                return getWhiteBalanceTemperatureDescription();
+            case TagCustomSaturation:
+                return getCustomSaturationDescription();
+            case TagModifiedSaturation:
+                return getModifiedSaturationDescription();
+            case TagContrastSetting:
+                return getContrastSettingDescription();
+            case TagSharpnessSetting:
+                return getSharpnessSettingDescription();
+            case TagColorSpace:
+                return getColorSpaceDescription();
+            case TagSceneMode:
+                return getSceneModeDescription();
+            case TagNoiseReduction:
+                return getNoiseReductionDescription();
+            case TagDistortionCorrection:
+                return getDistortionCorrectionDescription();
+            case TagShadingCompensation:
+                return getShadingCompensationDescription();
+            case TagGradation:
+                return getGradationDescription();
+            case TagPictureMode:
+                return getPictureModeDescription();
+            case TagPictureModeSaturation:
+                return getPictureModeSaturationDescription();
+            case TagPictureModeContrast:
+                return getPictureModeContrastDescription();
+            case TagPictureModeSharpness:
+                return getPictureModeSharpnessDescription();
+            case TagPictureModeBWFilter:
+                return getPictureModeBWFilterDescription();
+            case TagPictureModeTone:
+                return getPictureModeToneDescription();
+            case TagNoiseFilter:
+                return getNoiseFilterDescription();
+            case TagArtFilter:
+                return getArtFilterDescription();
+            case TagMagicFilter:
+                return getMagicFilterDescription();
+            case TagPictureModeEffect:
+                return getPictureModeEffectDescription();
+            case TagToneLevel:
+                return getToneLevelDescription();
+            case TagArtFilterEffect:
+                return getArtFilterEffectDescription();
+            case TagColorCreatorEffect:
+                return getColorCreatorEffectDescription();
+
+            case TagDriveMode:
+                return getDriveModeDescription();
+            case TagPanoramaMode:
+                return getPanoramaModeDescription();
+            case TagImageQuality2:
+                return getImageQuality2Description();
+            case TagImageStabilization:
+                return getImageStabilizationDescription();
+
+            case TagStackedImage:
+                return getStackedImageDescription();
+
+            case TagManometerPressure:
+                return getManometerPressureDescription();
+            case TagManometerReading:
+                return getManometerReadingDescription();
+            case TagExtendedWBDetect:
+                return getExtendedWBDetectDescription();
+            case TagRollAngle:
+                return getRollAngleDescription();
+            case TagPitchAngle:
+                return getPitchAngleDescription();
+            case TagDateTimeUtc:
+                return getDateTimeUTCDescription();
+
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getCameraSettingsVersionDescription()
+    {
+        return getVersionBytesDescription(TagCameraSettingsVersion, 4);
+    }
+
+    @Nullable
+    public String getPreviewImageValidDescription()
+    {
+        return getIndexedDescription(TagPreviewImageValid,
+            "No", "Yes");
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(TagExposureMode, 1,
+            "Manual", "Program", "Aperture-priority AE", "Shutter speed priority", "Program-shift");
+    }
+
+    @Nullable
+    public String getAeLockDescription()
+    {
+        return getIndexedDescription(TagAeLock,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        Integer value = _directory.getInteger(TagMeteringMode);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 2:
+                return "Center-weighted average";
+            case 3:
+                return "Spot";
+            case 5:
+                return "ESP";
+            case 261:
+                return "Pattern+AF";
+            case 515:
+                return "Spot+Highlight control";
+            case 1027:
+                return "Spot+Shadow control";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getExposureShiftDescription()
+    {
+        return getRationalOrDoubleString(TagExposureShift);
+    }
+
+    @Nullable
+    public String getNdFilterDescription()
+    {
+        return getIndexedDescription(TagNdFilter, "Off", "On");
+    }
+
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        return getIndexedDescription(TagMacroMode, "Off", "On", "Super Macro");
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagFocusMode);
+        if (values == null) {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagFocusMode);
+            if (value == null)
+                return null;
+
+            values = new int[]{value};
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        switch (values[0]) {
+            case 0:
+                sb.append("Single AF");
+                break;
+            case 1:
+                sb.append("Sequential shooting AF");
+                break;
+            case 2:
+                sb.append("Continuous AF");
+                break;
+            case 3:
+                sb.append("Multi AF");
+                break;
+            case 4:
+                sb.append("Face detect");
+                break;
+            case 10:
+                sb.append("MF");
+                break;
+            default:
+                sb.append("Unknown (" + values[0] + ")");
+                break;
+        }
+
+        if (values.length > 1) {
+            sb.append("; ");
+            int value1 = values[1];
+
+            if (value1 == 0) {
+                sb.append("(none)");
+            } else {
+                if (( value1       & 1) > 0) sb.append("S-AF, ");
+                if (((value1 >> 2) & 1) > 0) sb.append("C-AF, ");
+                if (((value1 >> 4) & 1) > 0) sb.append("MF, ");
+                if (((value1 >> 5) & 1) > 0) sb.append("Face detect, ");
+                if (((value1 >> 6) & 1) > 0) sb.append("Imager AF, ");
+                if (((value1 >> 7) & 1) > 0) sb.append("Live View Magnification Frame, ");
+                if (((value1 >> 8) & 1) > 0) sb.append("AF sensor, ");
+
+                sb.setLength(sb.length() - 2);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getFocusProcessDescription()
+    {
+        int[] values = _directory.getIntArray(TagFocusProcess);
+        if (values == null) {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagFocusProcess);
+            if (value == null)
+                return null;
+
+            values = new int[]{value};
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        switch (values[0]) {
+            case 0:
+                sb.append("AF not used");
+                break;
+            case 1:
+                sb.append("AF used");
+                break;
+            default:
+                sb.append("Unknown (" + values[0] + ")");
+                break;
+        }
+
+        if (values.length > 1)
+            sb.append("; " + values[1]);
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getAfSearchDescription()
+    {
+        return getIndexedDescription(TagAfSearch, "Not Ready", "Ready");
+    }
+
+    /// <summary>
+    /// coordinates range from 0 to 255
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getAfAreasDescription()
+    {
+        Object obj = _directory.getObject(TagAfAreas);
+        if (obj == null || !(obj instanceof long[]))
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (long point : (long[]) obj) {
+            if (point == 0L)
+                continue;
+            if (sb.length() != 0)
+                sb.append(", ");
+
+            if (point == 0x36794285L)
+                sb.append("Left ");
+            else if (point == 0x79798585L)
+                sb.append("Center ");
+            else if (point == 0xBD79C985L)
+                sb.append("Right ");
+
+            sb.append(String.format("(%d/255,%d/255)-(%d/255,%d/255)",
+                (point >> 24) & 0xFF,
+                (point >> 16) & 0xFF,
+                (point >> 8) & 0xFF,
+                point & 0xFF));
+        }
+
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    /// <summary>
+    /// coordinates expressed as a percent
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getAfPointSelectedDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TagAfPointSelected);
+        if (values == null)
+            return "n/a";
+
+        if (values.length < 4)
+            return null;
+
+        int index = 0;
+        if (values.length == 5 && values[0].longValue() == 0)
+            index = 1;
+
+        int p1 = (int)(values[index].doubleValue() * 100);
+        int p2 = (int)(values[index + 1].doubleValue() * 100);
+        int p3 = (int)(values[index + 2].doubleValue() * 100);
+        int p4 = (int)(values[index + 3].doubleValue() * 100);
+
+        if(p1 + p2 + p3 + p4 == 0)
+            return "n/a";
+
+        return String.format("(%d%%,%d%%) (%d%%,%d%%)", p1, p2, p3, p4);
+    }
+
+    @Nullable
+    public String getAfFineTuneDescription()
+    {
+        return getIndexedDescription(TagAfFineTune, "Off", "On");
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(TagFlashMode);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "Off";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if (( v       & 1) != 0) sb.append("On, ");
+        if (((v >> 1) & 1) != 0) sb.append("Fill-in, ");
+        if (((v >> 2) & 1) != 0) sb.append("Red-eye, ");
+        if (((v >> 3) & 1) != 0) sb.append("Slow-sync, ");
+        if (((v >> 4) & 1) != 0) sb.append("Forced On, ");
+        if (((v >> 5) & 1) != 0) sb.append("2nd Curtain, ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getFlashRemoteControlDescription()
+    {
+        Integer value = _directory.getInteger(TagFlashRemoteControl);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0:
+                return "Off";
+            case 0x01:
+                return "Channel 1, Low";
+            case 0x02:
+                return "Channel 2, Low";
+            case 0x03:
+                return "Channel 3, Low";
+            case 0x04:
+                return "Channel 4, Low";
+            case 0x09:
+                return "Channel 1, Mid";
+            case 0x0a:
+                return "Channel 2, Mid";
+            case 0x0b:
+                return "Channel 3, Mid";
+            case 0x0c:
+                return "Channel 4, Mid";
+            case 0x11:
+                return "Channel 1, High";
+            case 0x12:
+                return "Channel 2, High";
+            case 0x13:
+                return "Channel 3, High";
+            case 0x14:
+                return "Channel 4, High";
+
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    /// <summary>
+    /// 3 or 4 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getFlashControlModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagFlashControlMode);
+        if (values == null)
+            return null;
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        switch (values[0]) {
+            case 0:
+                sb.append("Off");
+                break;
+            case 3:
+                sb.append("TTL");
+                break;
+            case 4:
+                sb.append("Auto");
+                break;
+            case 5:
+                sb.append("Manual");
+                break;
+            default:
+                sb.append("Unknown (").append(values[0]).append(")");
+                break;
+        }
+
+        for (int i = 1; i < values.length; i++)
+            sb.append("; ").append(values[i]);
+
+        return sb.toString();
+    }
+
+    /// <summary>
+    /// 3 or 4 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getFlashIntensityDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TagFlashIntensity);
+        if (values == null || values.length == 0)
+            return null;
+
+        if (values.length == 3) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0)
+                return "n/a";
+        } else if (values.length == 4) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0)
+                return "n/a (x4)";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Rational t : values)
+            sb.append(t).append(", ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getManualFlashStrengthDescription()
+    {
+        Rational[] values = _directory.getRationalArray(TagManualFlashStrength);
+        if (values == null || values.length == 0)
+            return "n/a";
+
+        if (values.length == 3) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0)
+                return "n/a";
+        } else if (values.length == 4) {
+            if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0)
+                return "n/a (x4)";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Rational t : values)
+            sb.append(t).append(", ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getWhiteBalance2Description()
+    {
+        Integer value = _directory.getInteger(TagWhiteBalance2);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0:
+                return "Auto";
+            case 1:
+                return "Auto (Keep Warm Color Off)";
+            case 16:
+                return "7500K (Fine Weather with Shade)";
+            case 17:
+                return "6000K (Cloudy)";
+            case 18:
+                return "5300K (Fine Weather)";
+            case 20:
+                return "3000K (Tungsten light)";
+            case 21:
+                return "3600K (Tungsten light-like)";
+            case 22:
+                return "Auto Setup";
+            case 23:
+                return "5500K (Flash)";
+            case 33:
+                return "6600K (Daylight fluorescent)";
+            case 34:
+                return "4500K (Neutral white fluorescent)";
+            case 35:
+                return "4000K (Cool white fluorescent)";
+            case 36:
+                return "White Fluorescent";
+            case 48:
+                return "3600K (Tungsten light-like)";
+            case 67:
+                return "Underwater";
+            case 256:
+                return "One Touch WB 1";
+            case 257:
+                return "One Touch WB 2";
+            case 258:
+                return "One Touch WB 3";
+            case 259:
+                return "One Touch WB 4";
+            case 512:
+                return "Custom WB 1";
+            case 513:
+                return "Custom WB 2";
+            case 514:
+                return "Custom WB 3";
+            case 515:
+                return "Custom WB 4";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceTemperatureDescription()
+    {
+        Integer value = _directory.getInteger(TagWhiteBalanceTemperature);
+        if (value == null)
+            return null;
+        if (value == 0)
+            return "Auto";
+        return value.toString();
+    }
+
+    @Nullable
+    public String getCustomSaturationDescription()
+    {
+        // TODO: if model is /^E-1\b/  then
+        // $a-=$b; $c-=$b;
+        // return "CS$a (min CS0, max CS$c)"
+        return getValueMinMaxDescription(TagCustomSaturation);
+    }
+
+    @Nullable
+    public String getModifiedSaturationDescription()
+    {
+        return getIndexedDescription(TagModifiedSaturation,
+            "Off", "CM1 (Red Enhance)", "CM2 (Green Enhance)", "CM3 (Blue Enhance)", "CM4 (Skin Tones)");
+    }
+
+    @Nullable
+    public String getContrastSettingDescription()
+    {
+        return getValueMinMaxDescription(TagContrastSetting);
+    }
+
+    @Nullable
+    public String getSharpnessSettingDescription()
+    {
+        return getValueMinMaxDescription(TagSharpnessSetting);
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        return getIndexedDescription(TagColorSpace,
+            "sRGB", "Adobe RGB", "Pro Photo RGB");
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(TagSceneMode);
+        if (value == null)
+            return null;
+
+        switch (value) {
+            case 0:
+                return "Standard";
+            case 6:
+                return "Auto";
+            case 7:
+                return "Sport";
+            case 8:
+                return "Portrait";
+            case 9:
+                return "Landscape+Portrait";
+            case 10:
+                return "Landscape";
+            case 11:
+                return "Night Scene";
+            case 12:
+                return "Self Portrait";
+            case 13:
+                return "Panorama";
+            case 14:
+                return "2 in 1";
+            case 15:
+                return "Movie";
+            case 16:
+                return "Landscape+Portrait";
+            case 17:
+                return "Night+Portrait";
+            case 18:
+                return "Indoor";
+            case 19:
+                return "Fireworks";
+            case 20:
+                return "Sunset";
+            case 21:
+                return "Beauty Skin";
+            case 22:
+                return "Macro";
+            case 23:
+                return "Super Macro";
+            case 24:
+                return "Food";
+            case 25:
+                return "Documents";
+            case 26:
+                return "Museum";
+            case 27:
+                return "Shoot & Select";
+            case 28:
+                return "Beach & Snow";
+            case 29:
+                return "Self Portrait+Timer";
+            case 30:
+                return "Candle";
+            case 31:
+                return "Available Light";
+            case 32:
+                return "Behind Glass";
+            case 33:
+                return "My Mode";
+            case 34:
+                return "Pet";
+            case 35:
+                return "Underwater Wide1";
+            case 36:
+                return "Underwater Macro";
+            case 37:
+                return "Shoot & Select1";
+            case 38:
+                return "Shoot & Select2";
+            case 39:
+                return "High Key";
+            case 40:
+                return "Digital Image Stabilization";
+            case 41:
+                return "Auction";
+            case 42:
+                return "Beach";
+            case 43:
+                return "Snow";
+            case 44:
+                return "Underwater Wide2";
+            case 45:
+                return "Low Key";
+            case 46:
+                return "Children";
+            case 47:
+                return "Vivid";
+            case 48:
+                return "Nature Macro";
+            case 49:
+                return "Underwater Snapshot";
+            case 50:
+                return "Shooting Guide";
+            case 54:
+                return "Face Portrait";
+            case 57:
+                return "Bulb";
+            case 59:
+                return "Smile Shot";
+            case 60:
+                return "Quick Shutter";
+            case 63:
+                return "Slow Shutter";
+            case 64:
+                return "Bird Watching";
+            case 65:
+                return "Multiple Exposure";
+            case 66:
+                return "e-Portrait";
+            case 67:
+                return "Soft Background Shot";
+            case 142:
+                return "Hand-held Starlight";
+            case 154:
+                return "HDR";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TagNoiseReduction);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if ((v & 1) != 0) sb.append("Noise Reduction, ");
+        if (((v >> 1) & 1) != 0) sb.append("Noise Filter, ");
+        if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), ");
+        if (((v >> 3) & 1) != 0) sb.append("Auto, ");
+
+        return sb.length() != 0
+            ? sb.substring(0, sb.length() - 2)
+            : "(none)";
+    }
+
+    @Nullable
+    public String getDistortionCorrectionDescription()
+    {
+        return getIndexedDescription(TagDistortionCorrection, "Off", "On");
+    }
+
+    @Nullable
+    public String getShadingCompensationDescription()
+    {
+        return getIndexedDescription(TagShadingCompensation, "Off", "On");
+    }
+
+    /// <summary>
+    /// 3 or 4 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getGradationDescription()
+    {
+        int[] values = _directory.getIntArray(TagGradation);
+        if (values == null || values.length < 3)
+            return null;
+
+        String join = String.format("%d %d %d", values[0], values[1], values[2]);
+
+        String ret;
+        if (join.equals("0 0 0")) {
+            ret = "n/a";
+        } else if (join.equals("-1 -1 1")) {
+            ret = "Low Key";
+        } else if (join.equals("0 -1 1")) {
+            ret = "Normal";
+        } else if (join.equals("1 -1 1")) {
+            ret = "High Key";
+        } else {
+            ret = "Unknown (" + join + ")";
+        }
+
+        if (values.length > 3) {
+            if (values[3] == 0)
+                ret += "; User-Selected";
+            else if (values[3] == 1)
+                ret += "; Auto-Override";
+        }
+
+        return ret;
+    }
+
+    /// <summary>
+    /// 1 or 2 values
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getPictureModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagPictureMode);
+        if (values == null) {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagNoiseReduction);
+            if (value == null)
+                return null;
+
+            values = new int[]{value};
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        switch (values[0]) {
+            case 1:
+                sb.append("Vivid");
+                break;
+            case 2:
+                sb.append("Natural");
+                break;
+            case 3:
+                sb.append("Muted");
+                break;
+            case 4:
+                sb.append("Portrait");
+                break;
+            case 5:
+                sb.append("i-Enhance");
+                break;
+            case 256:
+                sb.append("Monotone");
+                break;
+            case 512:
+                sb.append("Sepia");
+                break;
+            default:
+                sb.append("Unknown (").append(values[0]).append(")");
+                break;
+        }
+
+        if (values.length > 1)
+            sb.append("; ").append(values[1]);
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getPictureModeSaturationDescription()
+    {
+        return getValueMinMaxDescription(TagPictureModeSaturation);
+    }
+
+    @Nullable
+    public String getPictureModeContrastDescription()
+    {
+        return getValueMinMaxDescription(TagPictureModeContrast);
+    }
+
+    @Nullable
+    public String getPictureModeSharpnessDescription()
+    {
+        return getValueMinMaxDescription(TagPictureModeSharpness);
+    }
+
+    @Nullable
+    public String getPictureModeBWFilterDescription()
+    {
+        return getIndexedDescription(TagPictureModeBWFilter,
+            "n/a", "Neutral", "Yellow", "Orange", "Red", "Green");
+    }
+
+    @Nullable
+    public String getPictureModeToneDescription()
+    {
+        return getIndexedDescription(TagPictureModeTone,
+            "n/a", "Neutral", "Sepia", "Blue", "Purple", "Green");
+    }
+
+    @Nullable
+    public String getNoiseFilterDescription()
+    {
+        int[] values = _directory.getIntArray(TagNoiseFilter);
+        if (values == null)
+            return null;
+
+        String join = String.format("%d %d %d", values[0], values[1], values[2]);
+
+        if (join.equals("0 0 0"))
+            return "n/a";
+        if (join.equals("-2 -2 1"))
+            return "Off";
+        if (join.equals("-1 -2 1"))
+            return "Low";
+        if (join.equals("0 -2 1"))
+            return "Standard";
+        if (join.equals("1 -2 1"))
+            return "High";
+        return "Unknown (" + join + ")";
+    }
+
+    @Nullable
+    public String getArtFilterDescription()
+    {
+        return getFiltersDescription(TagArtFilter);
+    }
+
+    @Nullable
+    public String getMagicFilterDescription()
+    {
+        return getFiltersDescription(TagMagicFilter);
+    }
+
+    @Nullable
+    public String getPictureModeEffectDescription()
+    {
+        int[] values = _directory.getIntArray(TagPictureModeEffect);
+        if (values == null)
+            return null;
+
+        String key = String.format("%d %d %d", values[0], values[1], values[2]);
+        if (key.equals("0 0 0"))
+            return "n/a";
+        if (key.equals("-1 -1 1"))
+            return "Low";
+        if (key.equals("0 -1 1"))
+            return "Standard";
+        if (key.equals("1 -1 1"))
+            return "High";
+        return "Unknown (" + key + ")";
+    }
+
+    @Nullable
+    public String getToneLevelDescription()
+    {
+        int[] values = _directory.getIntArray(TagToneLevel);
+        if (values == null || values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0 || i == 4 || i == 8 || i == 12 || i == 16 || i == 20 || i == 24)
+                sb.append(_toneLevelType.get(values[i])).append("; ");
+            else
+                sb.append(values[i]).append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getArtFilterEffectDescription()
+    {
+        int[] values = _directory.getIntArray(TagArtFilterEffect);
+        if (values == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0) {
+                sb.append((_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]")).append("; ");
+            } else if (i == 3) {
+                sb.append("Partial Color ").append(values[i]).append("; ");
+            } else if (i == 4) {
+                switch (values[i]) {
+                    case 0x0000:
+                        sb.append("No Effect");
+                        break;
+                    case 0x8010:
+                        sb.append("Star Light");
+                        break;
+                    case 0x8020:
+                        sb.append("Pin Hole");
+                        break;
+                    case 0x8030:
+                        sb.append("Frame");
+                        break;
+                    case 0x8040:
+                        sb.append("Soft Focus");
+                        break;
+                    case 0x8050:
+                        sb.append("White Edge");
+                        break;
+                    case 0x8060:
+                        sb.append("B&W");
+                        break;
+                    default:
+                        sb.append("Unknown (").append(values[i]).append(")");
+                        break;
+                }
+                sb.append("; ");
+            } else if (i == 6) {
+                switch (values[i]) {
+                    case 0:
+                        sb.append("No Color Filter");
+                        break;
+                    case 1:
+                        sb.append("Yellow Color Filter");
+                        break;
+                    case 2:
+                        sb.append("Orange Color Filter");
+                        break;
+                    case 3:
+                        sb.append("Red Color Filter");
+                        break;
+                    case 4:
+                        sb.append("Green Color Filter");
+                        break;
+                    default:
+                        sb.append("Unknown (").append(values[i]).append(")");
+                        break;
+                }
+                sb.append("; ");
+            } else {
+                sb.append(values[i]).append("; ");
+            }
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getColorCreatorEffectDescription()
+    {
+        int[] values = _directory.getIntArray(TagColorCreatorEffect);
+        if (values == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0) {
+                sb.append("Color ").append(values[i]).append("; ");
+            } else if (i == 3) {
+                sb.append("Strength ").append(values[i]).append("; ");
+            } else {
+                sb.append(values[i]).append("; ");
+            }
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    /// <summary>
+    /// 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getDriveModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagDriveMode);
+        if (values == null)
+            return null;
+
+        if (values.length == 0 || values[0] == 0)
+            return "Single Shot";
+
+        StringBuilder a = new StringBuilder();
+
+        if (values[0] == 5 && values.length >= 3) {
+            int c = values[2];
+            if (( c       & 1) > 0) a.append("AE");
+            if (((c >> 1) & 1) > 0) a.append("WB");
+            if (((c >> 2) & 1) > 0) a.append("FL");
+            if (((c >> 3) & 1) > 0) a.append("MF");
+            if (((c >> 6) & 1) > 0) a.append("Focus");
+
+            a.append(" Bracketing");
+        } else {
+            switch (values[0]) {
+                case 1:
+                    a.append("Continuous Shooting");
+                    break;
+                case 2:
+                    a.append("Exposure Bracketing");
+                    break;
+                case 3:
+                    a.append("White Balance Bracketing");
+                    break;
+                case 4:
+                    a.append("Exposure+WB Bracketing");
+                    break;
+                default:
+                    a.append("Unknown (").append(values[0]).append(")");
+                    break;
+            }
+        }
+
+        a.append(", Shot ").append(values[1]);
+
+        return a.toString();
+    }
+
+    /// <summary>
+    /// 2 numbers: 1. Mode, 2. Shot number
+    /// </summary>
+    /// <returns></returns>
+    @Nullable
+    public String getPanoramaModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagPanoramaMode);
+        if (values == null)
+            return null;
+
+        if (values.length == 0 || values[0] == 0)
+            return "Off";
+
+        String a;
+        switch (values[0]) {
+            case 1:
+                a = "Left to Right";
+                break;
+            case 2:
+                a = "Right to Left";
+                break;
+            case 3:
+                a = "Bottom to Top";
+                break;
+            case 4:
+                a = "Top to Bottom";
+                break;
+            default:
+                a = "Unknown (" + values[0] + ")";
+                break;
+        }
+
+        return String.format("%s, Shot %d", a, values[1]);
+    }
+
+    @Nullable
+    public String getImageQuality2Description()
+    {
+        return getIndexedDescription(TagImageQuality2, 1,
+            "SQ", "HQ", "SHQ", "RAW", "SQ (5)");
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        return getIndexedDescription(TagImageStabilization,
+            "Off", "On, Mode 1", "On, Mode 2", "On, Mode 3", "On, Mode 4");
+    }
+
+    @Nullable
+    public String getStackedImageDescription()
+    {
+        int[] values = _directory.getIntArray(TagStackedImage);
+        if (values == null || values.length < 2)
+            return null;
+
+        int v1 = values[0];
+        int v2 = values[1];
+
+        if (v1 == 0 && v2 == 0)
+            return "No";
+        if (v1 == 9 && v2 == 8)
+            return "Focus-stacked (8 images)";
+
+        return String.format("Unknown (%d %d)", v1, v2);
+    }
+
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getManometerPressureDescription()
+    {
+        Integer value = _directory.getInteger(TagManometerPressure);
+        if (value == null)
+            return null;
+
+        return String.format("%s kPa", new DecimalFormat("#.##").format(value / 10.0));
+    }
+
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getManometerReadingDescription()
+    {
+        int[] values = _directory.getIntArray(TagManometerReading);
+        if (values == null || values.length < 2)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("#.##");
+        return String.format("%s m, %s ft",
+            format.format(values[0] / 10.0),
+            format.format(values[1] / 10.0));
+    }
+
+    @Nullable
+    public String getExtendedWBDetectDescription()
+    {
+        return getIndexedDescription(TagExtendedWBDetect, "Off", "On");
+    }
+
+    /// <summary>
+    /// converted to degrees of clockwise camera rotation
+    /// </summary>
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getRollAngleDescription()
+    {
+        int[] values = _directory.getIntArray(TagRollAngle);
+        if (values == null || values.length < 2)
+            return null;
+
+        String ret = values[0] != 0
+            ? Double.toString(-values[0] / 10.0)
+            : "n/a";
+
+        return String.format("%s %d", ret, values[1]);
+    }
+
+    /// <summary>
+    /// converted to degrees of upward camera tilt
+    /// </summary>
+    /// <remarks>
+    /// TODO: need better image examples to test this function
+    /// </remarks>
+    /// <returns></returns>
+    @Nullable
+    public String getPitchAngleDescription()
+    {
+        int[] values = _directory.getIntArray(TagPitchAngle);
+        if (values == null || values.length < 2)
+            return null;
+
+        // (second value is 0 if level gauge is off)
+        String ret = values[0] != 0
+            ? Double.toString(values[0] / 10.0)
+            : "n/a";
+
+        return String.format("%s %d", ret, values[1]);
+    }
+
+    @Nullable
+    public String getDateTimeUTCDescription()
+    {
+        Object value = _directory.getObject(TagDateTimeUtc);
+        if (value == null)
+            return null;
+        return value.toString();
+    }
+
+    @Nullable
+    private String getValueMinMaxDescription(int tagId)
+    {
+        int[] values = _directory.getIntArray(tagId);
+        if (values == null || values.length < 3)
+            return null;
+
+        return String.format("%d (min %d, max %d)", values[0], values[1], values[2]);
+    }
+
+    @Nullable
+    private String getFiltersDescription(int tagId)
+    {
+        int[] values = _directory.getIntArray(tagId);
+        if (values == null || values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0)
+                sb.append(_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]");
+            else
+                sb.append(values[i]);
+            sb.append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    private static final HashMap<Integer, String> _toneLevelType = new HashMap<Integer, String>();
+    // ArtFilter, ArtFilterEffect and MagicFilter values
+    private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>();
+
+    static {
+        _filters.put(0, "Off");
+        _filters.put(1, "Soft Focus");
+        _filters.put(2, "Pop Art");
+        _filters.put(3, "Pale & Light Color");
+        _filters.put(4, "Light Tone");
+        _filters.put(5, "Pin Hole");
+        _filters.put(6, "Grainy Film");
+        _filters.put(9, "Diorama");
+        _filters.put(10, "Cross Process");
+        _filters.put(12, "Fish Eye");
+        _filters.put(13, "Drawing");
+        _filters.put(14, "Gentle Sepia");
+        _filters.put(15, "Pale & Light Color II");
+        _filters.put(16, "Pop Art II");
+        _filters.put(17, "Pin Hole II");
+        _filters.put(18, "Pin Hole III");
+        _filters.put(19, "Grainy Film II");
+        _filters.put(20, "Dramatic Tone");
+        _filters.put(21, "Punk");
+        _filters.put(22, "Soft Focus 2");
+        _filters.put(23, "Sparkle");
+        _filters.put(24, "Watercolor");
+        _filters.put(25, "Key Line");
+        _filters.put(26, "Key Line II");
+        _filters.put(27, "Miniature");
+        _filters.put(28, "Reflection");
+        _filters.put(29, "Fragmented");
+        _filters.put(31, "Cross Process II");
+        _filters.put(32, "Dramatic Tone II");
+        _filters.put(33, "Watercolor I");
+        _filters.put(34, "Watercolor II");
+        _filters.put(35, "Diorama II");
+        _filters.put(36, "Vintage");
+        _filters.put(37, "Vintage II");
+        _filters.put(38, "Vintage III");
+        _filters.put(39, "Partial Color");
+        _filters.put(40, "Partial Color II");
+        _filters.put(41, "Partial Color III");
+
+        _toneLevelType.put(0, "0");
+        _toneLevelType.put(-31999, "Highlights ");
+        _toneLevelType.put(-31998, "Shadows ");
+        _toneLevelType.put(-31997, "Midtones ");
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java
new file mode 100644
index 0000000..90247c5
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * The Olympus camera settings makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusCameraSettingsMakernoteDirectory extends Directory
+{
+    public static final int TagCameraSettingsVersion = 0x0000;
+    public static final int TagPreviewImageValid = 0x0100;
+    public static final int TagPreviewImageStart = 0x0101;
+    public static final int TagPreviewImageLength = 0x0102;
+
+    public static final int TagExposureMode = 0x0200;
+    public static final int TagAeLock = 0x0201;
+    public static final int TagMeteringMode = 0x0202;
+    public static final int TagExposureShift = 0x0203;
+    public static final int TagNdFilter = 0x0204;
+
+    public static final int TagMacroMode = 0x0300;
+    public static final int TagFocusMode = 0x0301;
+    public static final int TagFocusProcess = 0x0302;
+    public static final int TagAfSearch = 0x0303;
+    public static final int TagAfAreas = 0x0304;
+    public static final int TagAfPointSelected = 0x0305;
+    public static final int TagAfFineTune = 0x0306;
+    public static final int TagAfFineTuneAdj = 0x0307;
+
+    public static final int TagFlashMode = 0x400;
+    public static final int TagFlashExposureComp = 0x401;
+    public static final int TagFlashRemoteControl = 0x403;
+    public static final int TagFlashControlMode = 0x404;
+    public static final int TagFlashIntensity = 0x405;
+    public static final int TagManualFlashStrength = 0x406;
+
+    public static final int TagWhiteBalance2 = 0x500;
+    public static final int TagWhiteBalanceTemperature = 0x501;
+    public static final int TagWhiteBalanceBracket = 0x502;
+    public static final int TagCustomSaturation = 0x503;
+    public static final int TagModifiedSaturation = 0x504;
+    public static final int TagContrastSetting = 0x505;
+    public static final int TagSharpnessSetting = 0x506;
+    public static final int TagColorSpace = 0x507;
+    public static final int TagSceneMode = 0x509;
+    public static final int TagNoiseReduction = 0x50a;
+    public static final int TagDistortionCorrection = 0x50b;
+    public static final int TagShadingCompensation = 0x50c;
+    public static final int TagCompressionFactor = 0x50d;
+    public static final int TagGradation = 0x50f;
+    public static final int TagPictureMode = 0x520;
+    public static final int TagPictureModeSaturation = 0x521;
+    public static final int TagPictureModeHue = 0x522;
+    public static final int TagPictureModeContrast = 0x523;
+    public static final int TagPictureModeSharpness = 0x524;
+    public static final int TagPictureModeBWFilter = 0x525;
+    public static final int TagPictureModeTone = 0x526;
+    public static final int TagNoiseFilter = 0x527;
+    public static final int TagArtFilter = 0x529;
+    public static final int TagMagicFilter = 0x52c;
+    public static final int TagPictureModeEffect = 0x52d;
+    public static final int TagToneLevel = 0x52e;
+    public static final int TagArtFilterEffect = 0x52f;
+    public static final int TagColorCreatorEffect = 0x532;
+
+    public static final int TagDriveMode = 0x600;
+    public static final int TagPanoramaMode = 0x601;
+    public static final int TagImageQuality2 = 0x603;
+    public static final int TagImageStabilization = 0x604;
+
+    public static final int TagStackedImage = 0x804;
+
+    public static final int TagManometerPressure = 0x900;
+    public static final int TagManometerReading = 0x901;
+    public static final int TagExtendedWBDetect = 0x902;
+    public static final int TagRollAngle = 0x903;
+    public static final int TagPitchAngle = 0x904;
+    public static final int TagDateTimeUtc = 0x908;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagCameraSettingsVersion, "Camera Settings Version");
+        _tagNameMap.put(TagPreviewImageValid, "Preview Image Valid");
+        _tagNameMap.put(TagPreviewImageStart, "Preview Image Start");
+        _tagNameMap.put(TagPreviewImageLength, "Preview Image Length");
+
+        _tagNameMap.put(TagExposureMode, "Exposure Mode");
+        _tagNameMap.put(TagAeLock, "AE Lock");
+        _tagNameMap.put(TagMeteringMode, "Metering Mode");
+        _tagNameMap.put(TagExposureShift, "Exposure Shift");
+        _tagNameMap.put(TagNdFilter, "ND Filter");
+
+        _tagNameMap.put(TagMacroMode, "Macro Mode");
+        _tagNameMap.put(TagFocusMode, "Focus Mode");
+        _tagNameMap.put(TagFocusProcess, "Focus Process");
+        _tagNameMap.put(TagAfSearch, "AF Search");
+        _tagNameMap.put(TagAfAreas, "AF Areas");
+        _tagNameMap.put(TagAfPointSelected, "AF Point Selected");
+        _tagNameMap.put(TagAfFineTune, "AF Fine Tune");
+        _tagNameMap.put(TagAfFineTuneAdj, "AF Fine Tune Adj");
+
+        _tagNameMap.put(TagFlashMode, "Flash Mode");
+        _tagNameMap.put(TagFlashExposureComp, "Flash Exposure Comp");
+        _tagNameMap.put(TagFlashRemoteControl, "Flash Remote Control");
+        _tagNameMap.put(TagFlashControlMode, "Flash Control Mode");
+        _tagNameMap.put(TagFlashIntensity, "Flash Intensity");
+        _tagNameMap.put(TagManualFlashStrength, "Manual Flash Strength");
+
+        _tagNameMap.put(TagWhiteBalance2, "White Balance 2");
+        _tagNameMap.put(TagWhiteBalanceTemperature, "White Balance Temperature");
+        _tagNameMap.put(TagWhiteBalanceBracket, "White Balance Bracket");
+        _tagNameMap.put(TagCustomSaturation, "Custom Saturation");
+        _tagNameMap.put(TagModifiedSaturation, "Modified Saturation");
+        _tagNameMap.put(TagContrastSetting, "Contrast Setting");
+        _tagNameMap.put(TagSharpnessSetting, "Sharpness Setting");
+        _tagNameMap.put(TagColorSpace, "Color Space");
+        _tagNameMap.put(TagSceneMode, "Scene Mode");
+        _tagNameMap.put(TagNoiseReduction, "Noise Reduction");
+        _tagNameMap.put(TagDistortionCorrection, "Distortion Correction");
+        _tagNameMap.put(TagShadingCompensation, "Shading Compensation");
+        _tagNameMap.put(TagCompressionFactor, "Compression Factor");
+        _tagNameMap.put(TagGradation, "Gradation");
+        _tagNameMap.put(TagPictureMode, "Picture Mode");
+        _tagNameMap.put(TagPictureModeSaturation, "Picture Mode Saturation");
+        _tagNameMap.put(TagPictureModeHue, "Picture Mode Hue");
+        _tagNameMap.put(TagPictureModeContrast, "Picture Mode Contrast");
+        _tagNameMap.put(TagPictureModeSharpness, "Picture Mode Sharpness");
+        _tagNameMap.put(TagPictureModeBWFilter, "Picture Mode BW Filter");
+        _tagNameMap.put(TagPictureModeTone, "Picture Mode Tone");
+        _tagNameMap.put(TagNoiseFilter, "Noise Filter");
+        _tagNameMap.put(TagArtFilter, "Art Filter");
+        _tagNameMap.put(TagMagicFilter, "Magic Filter");
+        _tagNameMap.put(TagPictureModeEffect, "Picture Mode Effect");
+        _tagNameMap.put(TagToneLevel, "Tone Level");
+        _tagNameMap.put(TagArtFilterEffect, "Art Filter Effect");
+        _tagNameMap.put(TagColorCreatorEffect, "Color Creator Effect");
+
+        _tagNameMap.put(TagDriveMode, "Drive Mode");
+        _tagNameMap.put(TagPanoramaMode, "Panorama Mode");
+        _tagNameMap.put(TagImageQuality2, "Image Quality 2");
+        _tagNameMap.put(TagImageStabilization, "Image Stabilization");
+
+        _tagNameMap.put(TagStackedImage, "Stacked Image");
+
+        _tagNameMap.put(TagManometerPressure, "Manometer Pressure");
+        _tagNameMap.put(TagManometerReading, "Manometer Reading");
+        _tagNameMap.put(TagExtendedWBDetect, "Extended WB Detect");
+        _tagNameMap.put(TagRollAngle, "Roll Angle");
+        _tagNameMap.put(TagPitchAngle, "Pitch Angle");
+        _tagNameMap.put(TagDateTimeUtc, "Date Time UTC");
+    }
+
+    public OlympusCameraSettingsMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusCameraSettingsMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Camera Settings";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java
new file mode 100644
index 0000000..e37302d
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DecimalFormat;
+import java.util.HashMap;
+
+import static com.drew.metadata.exif.makernotes.OlympusEquipmentMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusEquipmentMakernoteDirectory}.
+ * <p>
+ * Some Description functions and the Extender and Lens types lists converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusEquipmentMakernoteDescriptor extends TagDescriptor<OlympusEquipmentMakernoteDirectory>
+{
+    public OlympusEquipmentMakernoteDescriptor(@NotNull OlympusEquipmentMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_EQUIPMENT_VERSION:
+                return getEquipmentVersionDescription();
+            case TAG_CAMERA_TYPE_2:
+                return getCameraType2Description();
+            case TAG_FOCAL_PLANE_DIAGONAL:
+                return getFocalPlaneDiagonalDescription();
+            case TAG_BODY_FIRMWARE_VERSION:
+                return getBodyFirmwareVersionDescription();
+            case TAG_LENS_TYPE:
+                return getLensTypeDescription();
+            case TAG_LENS_FIRMWARE_VERSION:
+                return getLensFirmwareVersionDescription();
+            case TAG_MAX_APERTURE_AT_MIN_FOCAL:
+                return getMaxApertureAtMinFocalDescription();
+            case TAG_MAX_APERTURE_AT_MAX_FOCAL:
+                return getMaxApertureAtMaxFocalDescription();
+            case TAG_MAX_APERTURE:
+                return getMaxApertureDescription();
+            case TAG_LENS_PROPERTIES:
+                return getLensPropertiesDescription();
+            case TAG_EXTENDER:
+                return getExtenderDescription();
+            case TAG_FLASH_TYPE:
+                return getFlashTypeDescription();
+            case TAG_FLASH_MODEL:
+                return getFlashModelDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getEquipmentVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EQUIPMENT_VERSION, 4);
+    }
+
+    @Nullable
+    public String getCameraType2Description()
+    {
+        String cameratype = _directory.getString(TAG_CAMERA_TYPE_2);
+        if(cameratype == null)
+            return null;
+
+        if(OlympusMakernoteDirectory.OlympusCameraTypes.containsKey(cameratype))
+            return OlympusMakernoteDirectory.OlympusCameraTypes.get(cameratype);
+
+        return cameratype;
+    }
+
+    @Nullable
+    public String getFocalPlaneDiagonalDescription()
+    {
+        return _directory.getString(TAG_FOCAL_PLANE_DIAGONAL) + " mm";
+    }
+
+    @Nullable
+    public String getBodyFirmwareVersionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_BODY_FIRMWARE_VERSION);
+        if (value == null)
+            return null;
+
+        String hex = String.format("%04X", value);
+        return String.format("%s.%s",
+            hex.substring(0, hex.length() - 3),
+            hex.substring(hex.length() - 3));
+    }
+
+    @Nullable
+    public String getLensTypeDescription()
+    {
+        String str = _directory.getString(TAG_LENS_TYPE);
+
+        if (str == null)
+            return null;
+
+        // The String contains six numbers:
+        //
+        // - Make
+        // - Unknown
+        // - Model
+        // - Sub-model
+        // - Unknown
+        // - Unknown
+        //
+        // Only the Make, Model and Sub-model are used to identify the lens type
+        String[] values = str.split(" ");
+
+        if (values.length < 6)
+            return null;
+
+        try {
+            int num1 = Integer.parseInt(values[0]);
+            int num2 = Integer.parseInt(values[2]);
+            int num3 = Integer.parseInt(values[3]);
+            return _olympusLensTypes.get(String.format("%X %02X %02X", num1, num2, num3));
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getLensFirmwareVersionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_LENS_FIRMWARE_VERSION);
+        if (value == null)
+            return null;
+
+        String hex = String.format("%04X", value);
+        return String.format("%s.%s",
+            hex.substring(0, hex.length() - 3),
+            hex.substring(hex.length() - 3));
+    }
+
+    @Nullable
+    public String getMaxApertureAtMinFocalDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MIN_FOCAL);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        return format.format(CalcMaxAperture(value));
+    }
+
+    @Nullable
+    public String getMaxApertureAtMaxFocalDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MAX_FOCAL);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        return format.format(CalcMaxAperture(value));
+    }
+
+    @Nullable
+    public String getMaxApertureDescription()
+    {
+        Integer value = _directory.getInteger(TAG_MAX_APERTURE);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        return format.format(CalcMaxAperture(value));
+    }
+
+    private static double CalcMaxAperture(int value)
+    {
+        return Math.pow(Math.sqrt(2.00), value / 256.0);
+    }
+
+    @Nullable
+    public String getLensPropertiesDescription()
+    {
+        Integer value = _directory.getInteger(TAG_LENS_PROPERTIES);
+        if (value == null)
+            return null;
+
+        return String.format("0x%04X", value);
+    }
+
+    @Nullable
+    public String getExtenderDescription()
+    {
+        String str = _directory.getString(TAG_EXTENDER);
+
+        if (str == null)
+            return null;
+
+        // The String contains six numbers:
+        //
+        // - Make
+        // - Unknown
+        // - Model
+        // - Sub-model
+        // - Unknown
+        // - Unknown
+        //
+        // Only the Make and Model are used to identify the extender
+        String[] values = str.split(" ");
+
+        if (values.length < 6)
+            return null;
+
+        try {
+            int num1 = Integer.parseInt(values[0]);
+            int num2 = Integer.parseInt(values[2]);
+            String extenderType = String.format("%X %02X", num1, num2);
+            return _olympusExtenderTypes.get(extenderType);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getFlashTypeDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_TYPE,
+            "None", null, "Simple E-System", "E-System");
+    }
+
+    @Nullable
+    public String getFlashModelDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_MODEL,
+            "None", "FL-20", "FL-50", "RF-11", "TF-22", "FL-36", "FL-50R", "FL-36R");
+    }
+
+    private static final HashMap<String, String> _olympusLensTypes = new HashMap<String, String>();
+    private static final HashMap<String, String> _olympusExtenderTypes = new HashMap<String, String>();
+
+    static {
+        _olympusLensTypes.put("0 00 00", "None");
+        // Olympus lenses (also Kenko Tokina)
+        _olympusLensTypes.put("0 01 00", "Olympus Zuiko Digital ED 50mm F2.0 Macro");
+        _olympusLensTypes.put("0 01 01", "Olympus Zuiko Digital 40-150mm F3.5-4.5"); //8
+        _olympusLensTypes.put("0 01 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6"); //PH (E-P1 pre-production)
+        _olympusLensTypes.put("0 02 00", "Olympus Zuiko Digital ED 150mm F2.0");
+        _olympusLensTypes.put("0 02 10", "Olympus M.Zuiko Digital 17mm F2.8 Pancake"); //PH (E-P1 pre-production)
+        _olympusLensTypes.put("0 03 00", "Olympus Zuiko Digital ED 300mm F2.8");
+        _olympusLensTypes.put("0 03 10", "Olympus M.Zuiko Digital ED 14-150mm F4.0-5.6 [II]"); //11 (The second version of this lens seems to have the same lens ID number as the first version #20)
+        _olympusLensTypes.put("0 04 10", "Olympus M.Zuiko Digital ED 9-18mm F4.0-5.6"); //11
+        _olympusLensTypes.put("0 05 00", "Olympus Zuiko Digital 14-54mm F2.8-3.5");
+        _olympusLensTypes.put("0 05 01", "Olympus Zuiko Digital Pro ED 90-250mm F2.8"); //9
+        _olympusLensTypes.put("0 05 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6 L"); //11 (E-PL1)
+        _olympusLensTypes.put("0 06 00", "Olympus Zuiko Digital ED 50-200mm F2.8-3.5");
+        _olympusLensTypes.put("0 06 01", "Olympus Zuiko Digital ED 8mm F3.5 Fisheye"); //9
+        _olympusLensTypes.put("0 06 10", "Olympus M.Zuiko Digital ED 40-150mm F4.0-5.6"); //PH
+        _olympusLensTypes.put("0 07 00", "Olympus Zuiko Digital 11-22mm F2.8-3.5");
+        _olympusLensTypes.put("0 07 01", "Olympus Zuiko Digital 18-180mm F3.5-6.3"); //6
+        _olympusLensTypes.put("0 07 10", "Olympus M.Zuiko Digital ED 12mm F2.0"); //PH
+        _olympusLensTypes.put("0 08 01", "Olympus Zuiko Digital 70-300mm F4.0-5.6"); //7 (seen as release 1 - PH)
+        _olympusLensTypes.put("0 08 10", "Olympus M.Zuiko Digital ED 75-300mm F4.8-6.7"); //PH
+        _olympusLensTypes.put("0 09 10", "Olympus M.Zuiko Digital 14-42mm F3.5-5.6 II"); //PH (E-PL2)
+        _olympusLensTypes.put("0 10 01", "Kenko Tokina Reflex 300mm F6.3 MF Macro"); //20
+        _olympusLensTypes.put("0 10 10", "Olympus M.Zuiko Digital ED 12-50mm F3.5-6.3 EZ"); //PH
+        _olympusLensTypes.put("0 11 10", "Olympus M.Zuiko Digital 45mm F1.8"); //17
+        _olympusLensTypes.put("0 12 10", "Olympus M.Zuiko Digital ED 60mm F2.8 Macro"); //20
+        _olympusLensTypes.put("0 13 10", "Olympus M.Zuiko Digital 14-42mm F3.5-5.6 II R"); //PH/20
+        _olympusLensTypes.put("0 14 10", "Olympus M.Zuiko Digital ED 40-150mm F4.0-5.6 R"); //19
+        // '0 14 10.1", "Olympus M.Zuiko Digital ED 14-150mm F4.0-5.6 II"); //11 (questionable & unconfirmed -- all samples I can find are '0 3 10' - PH)
+        _olympusLensTypes.put("0 15 00", "Olympus Zuiko Digital ED 7-14mm F4.0");
+        _olympusLensTypes.put("0 15 10", "Olympus M.Zuiko Digital ED 75mm F1.8"); //PH
+        _olympusLensTypes.put("0 16 10", "Olympus M.Zuiko Digital 17mm F1.8"); //20
+        _olympusLensTypes.put("0 17 00", "Olympus Zuiko Digital Pro ED 35-100mm F2.0"); //7
+        _olympusLensTypes.put("0 18 00", "Olympus Zuiko Digital 14-45mm F3.5-5.6");
+        _olympusLensTypes.put("0 18 10", "Olympus M.Zuiko Digital ED 75-300mm F4.8-6.7 II"); //20
+        _olympusLensTypes.put("0 19 10", "Olympus M.Zuiko Digital ED 12-40mm F2.8 Pro"); //PH
+        _olympusLensTypes.put("0 20 00", "Olympus Zuiko Digital 35mm F3.5 Macro"); //9
+        _olympusLensTypes.put("0 20 10", "Olympus M.Zuiko Digital ED 40-150mm F2.8 Pro"); //20
+        _olympusLensTypes.put("0 21 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6 EZ"); //20
+        _olympusLensTypes.put("0 22 00", "Olympus Zuiko Digital 17.5-45mm F3.5-5.6"); //9
+        _olympusLensTypes.put("0 22 10", "Olympus M.Zuiko Digital 25mm F1.8"); //20
+        _olympusLensTypes.put("0 23 00", "Olympus Zuiko Digital ED 14-42mm F3.5-5.6"); //PH
+        _olympusLensTypes.put("0 23 10", "Olympus M.Zuiko Digital ED 7-14mm F2.8 Pro"); //20
+        _olympusLensTypes.put("0 24 00", "Olympus Zuiko Digital ED 40-150mm F4.0-5.6"); //PH
+        _olympusLensTypes.put("0 24 10", "Olympus M.Zuiko Digital ED 300mm F4.0 IS Pro"); //20
+        _olympusLensTypes.put("0 25 10", "Olympus M.Zuiko Digital ED 8mm F1.8 Fisheye Pro"); //20
+        _olympusLensTypes.put("0 30 00", "Olympus Zuiko Digital ED 50-200mm F2.8-3.5 SWD"); //7
+        _olympusLensTypes.put("0 31 00", "Olympus Zuiko Digital ED 12-60mm F2.8-4.0 SWD"); //7
+        _olympusLensTypes.put("0 32 00", "Olympus Zuiko Digital ED 14-35mm F2.0 SWD"); //PH
+        _olympusLensTypes.put("0 33 00", "Olympus Zuiko Digital 25mm F2.8"); //PH
+        _olympusLensTypes.put("0 34 00", "Olympus Zuiko Digital ED 9-18mm F4.0-5.6"); //7
+        _olympusLensTypes.put("0 35 00", "Olympus Zuiko Digital 14-54mm F2.8-3.5 II"); //PH
+        // Sigma lenses
+        _olympusLensTypes.put("1 01 00", "Sigma 18-50mm F3.5-5.6 DC"); //8
+        _olympusLensTypes.put("1 01 10", "Sigma 30mm F2.8 EX DN"); //20
+        _olympusLensTypes.put("1 02 00", "Sigma 55-200mm F4.0-5.6 DC");
+        _olympusLensTypes.put("1 02 10", "Sigma 19mm F2.8 EX DN"); //20
+        _olympusLensTypes.put("1 03 00", "Sigma 18-125mm F3.5-5.6 DC");
+        _olympusLensTypes.put("1 03 10", "Sigma 30mm F2.8 DN | A"); //20
+        _olympusLensTypes.put("1 04 00", "Sigma 18-125mm F3.5-5.6 DC"); //7
+        _olympusLensTypes.put("1 04 10", "Sigma 19mm F2.8 DN | A"); //20
+        _olympusLensTypes.put("1 05 00", "Sigma 30mm F1.4 EX DC HSM"); //10
+        _olympusLensTypes.put("1 05 10", "Sigma 60mm F2.8 DN | A"); //20
+        _olympusLensTypes.put("1 06 00", "Sigma APO 50-500mm F4.0-6.3 EX DG HSM"); //6
+        _olympusLensTypes.put("1 07 00", "Sigma Macro 105mm F2.8 EX DG"); //PH
+        _olympusLensTypes.put("1 08 00", "Sigma APO Macro 150mm F2.8 EX DG HSM"); //PH
+        _olympusLensTypes.put("1 09 00", "Sigma 18-50mm F2.8 EX DC Macro"); //20
+        _olympusLensTypes.put("1 10 00", "Sigma 24mm F1.8 EX DG Aspherical Macro"); //PH
+        _olympusLensTypes.put("1 11 00", "Sigma APO 135-400mm F4.5-5.6 DG"); //11
+        _olympusLensTypes.put("1 12 00", "Sigma APO 300-800mm F5.6 EX DG HSM"); //11
+        _olympusLensTypes.put("1 13 00", "Sigma 30mm F1.4 EX DC HSM"); //11
+        _olympusLensTypes.put("1 14 00", "Sigma APO 50-500mm F4.0-6.3 EX DG HSM"); //11
+        _olympusLensTypes.put("1 15 00", "Sigma 10-20mm F4.0-5.6 EX DC HSM"); //11
+        _olympusLensTypes.put("1 16 00", "Sigma APO 70-200mm F2.8 II EX DG Macro HSM"); //11
+        _olympusLensTypes.put("1 17 00", "Sigma 50mm F1.4 EX DG HSM"); //11
+        // Panasonic/Leica lenses
+        _olympusLensTypes.put("2 01 00", "Leica D Vario Elmarit 14-50mm F2.8-3.5 Asph."); //11
+        _olympusLensTypes.put("2 01 10", "Lumix G Vario 14-45mm F3.5-5.6 Asph. Mega OIS"); //16
+        _olympusLensTypes.put("2 02 00", "Leica D Summilux 25mm F1.4 Asph."); //11
+        _olympusLensTypes.put("2 02 10", "Lumix G Vario 45-200mm F4.0-5.6 Mega OIS"); //16
+        _olympusLensTypes.put("2 03 00", "Leica D Vario Elmar 14-50mm F3.8-5.6 Asph. Mega OIS"); //11
+        _olympusLensTypes.put("2 03 01", "Leica D Vario Elmar 14-50mm F3.8-5.6 Asph."); //14 (L10 kit)
+        _olympusLensTypes.put("2 03 10", "Lumix G Vario HD 14-140mm F4.0-5.8 Asph. Mega OIS"); //16
+        _olympusLensTypes.put("2 04 00", "Leica D Vario Elmar 14-150mm F3.5-5.6"); //13
+        _olympusLensTypes.put("2 04 10", "Lumix G Vario 7-14mm F4.0 Asph."); //PH (E-P1 pre-production)
+        _olympusLensTypes.put("2 05 10", "Lumix G 20mm F1.7 Asph."); //16
+        _olympusLensTypes.put("2 06 10", "Leica DG Macro-Elmarit 45mm F2.8 Asph. Mega OIS"); //PH
+        _olympusLensTypes.put("2 07 10", "Lumix G Vario 14-42mm F3.5-5.6 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 08 10", "Lumix G Fisheye 8mm F3.5"); //PH
+        _olympusLensTypes.put("2 09 10", "Lumix G Vario 100-300mm F4.0-5.6 Mega OIS"); //11
+        _olympusLensTypes.put("2 10 10", "Lumix G 14mm F2.5 Asph."); //17
+        _olympusLensTypes.put("2 11 10", "Lumix G 12.5mm F12 3D"); //20 (H-FT012)
+        _olympusLensTypes.put("2 12 10", "Leica DG Summilux 25mm F1.4 Asph."); //20
+        _olympusLensTypes.put("2 13 10", "Lumix G X Vario PZ 45-175mm F4.0-5.6 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 14 10", "Lumix G X Vario PZ 14-42mm F3.5-5.6 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 15 10", "Lumix G X Vario 12-35mm F2.8 Asph. Power OIS"); //PH
+        _olympusLensTypes.put("2 16 10", "Lumix G Vario 45-150mm F4.0-5.6 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 17 10", "Lumix G X Vario 35-100mm F2.8 Power OIS"); //PH
+        _olympusLensTypes.put("2 18 10", "Lumix G Vario 14-42mm F3.5-5.6 II Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 19 10", "Lumix G Vario 14-140mm F3.5-5.6 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 20 10", "Lumix G Vario 12-32mm F3.5-5.6 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 21 10", "Leica DG Nocticron 42.5mm F1.2 Asph. Power OIS"); //20
+        _olympusLensTypes.put("2 22 10", "Leica DG Summilux 15mm F1.7 Asph."); //20
+        // '2 23 10", "Lumix G Vario 35-100mm F4.0-5.6 Asph. Mega OIS"); //20 (guess)
+        _olympusLensTypes.put("2 24 10", "Lumix G Macro 30mm F2.8 Asph. Mega OIS"); //20
+        _olympusLensTypes.put("2 25 10", "Lumix G 42.5mm F1.7 Asph. Power OIS"); //20
+        _olympusLensTypes.put("3 01 00", "Leica D Vario Elmarit 14-50mm F2.8-3.5 Asph."); //11
+        _olympusLensTypes.put("3 02 00", "Leica D Summilux 25mm F1.4 Asph."); //11
+        // Tamron lenses
+        _olympusLensTypes.put("5 01 10", "Tamron 14-150mm F3.5-5.8 Di III"); //20 (model C001)
+
+
+        _olympusExtenderTypes.put("0 00", "None");
+        _olympusExtenderTypes.put("0 04", "Olympus Zuiko Digital EC-14 1.4x Teleconverter");
+        _olympusExtenderTypes.put("0 08", "Olympus EX-25 Extension Tube");
+        _olympusExtenderTypes.put("0 10", "Olympus Zuiko Digital EC-20 2.0x Teleconverter");
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java
new file mode 100644
index 0000000..eb3d50c
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * The Olympus equipment makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusEquipmentMakernoteDirectory extends Directory
+{
+    public static final int TAG_EQUIPMENT_VERSION = 0x0000;
+    public static final int TAG_CAMERA_TYPE_2 = 0x0100;
+    public static final int TAG_SERIAL_NUMBER = 0x0101;
+
+    public static final int TAG_INTERNAL_SERIAL_NUMBER = 0x0102;
+    public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0103;
+    public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104;
+
+    public static final int TAG_LENS_TYPE = 0x0201;
+    public static final int TAG_LENS_SERIAL_NUMBER = 0x0202;
+    public static final int TAG_LENS_MODEL = 0x0203;
+    public static final int TAG_LENS_FIRMWARE_VERSION = 0x0204;
+    public static final int TAG_MAX_APERTURE_AT_MIN_FOCAL = 0x0205;
+    public static final int TAG_MAX_APERTURE_AT_MAX_FOCAL = 0x0206;
+    public static final int TAG_MIN_FOCAL_LENGTH = 0x0207;
+    public static final int TAG_MAX_FOCAL_LENGTH = 0x0208;
+    public static final int TAG_MAX_APERTURE = 0x020A;
+    public static final int TAG_LENS_PROPERTIES = 0x020B;
+
+    public static final int TAG_EXTENDER = 0x0301;
+    public static final int TAG_EXTENDER_SERIAL_NUMBER = 0x0302;
+    public static final int TAG_EXTENDER_MODEL = 0x0303;
+    public static final int TAG_EXTENDER_FIRMWARE_VERSION = 0x0304;
+
+    public static final int TAG_CONVERSION_LENS = 0x0403;
+
+    public static final int TAG_FLASH_TYPE = 0x1000;
+    public static final int TAG_FLASH_MODEL = 0x1001;
+    public static final int TAG_FLASH_FIRMWARE_VERSION = 0x1002;
+    public static final int TAG_FLASH_SERIAL_NUMBER = 0x1003;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_EQUIPMENT_VERSION, "Equipment Version");
+        _tagNameMap.put(TAG_CAMERA_TYPE_2, "Camera Type 2");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_INTERNAL_SERIAL_NUMBER, "Internal Serial Number");
+        _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
+        _tagNameMap.put(TAG_BODY_FIRMWARE_VERSION, "Body Firmware Version");
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        _tagNameMap.put(TAG_LENS_MODEL, "Lens Model");
+        _tagNameMap.put(TAG_LENS_FIRMWARE_VERSION, "Lens Firmware Version");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MIN_FOCAL, "Max Aperture At Min Focal");
+        _tagNameMap.put(TAG_MAX_APERTURE_AT_MAX_FOCAL, "Max Aperture At Max Focal");
+        _tagNameMap.put(TAG_MIN_FOCAL_LENGTH, "Min Focal Length");
+        _tagNameMap.put(TAG_MAX_FOCAL_LENGTH, "Max Focal Length");
+        _tagNameMap.put(TAG_MAX_APERTURE, "Max Aperture");
+        _tagNameMap.put(TAG_LENS_PROPERTIES, "Lens Properties");
+        _tagNameMap.put(TAG_EXTENDER, "Extender");
+        _tagNameMap.put(TAG_EXTENDER_SERIAL_NUMBER, "Extender Serial Number");
+        _tagNameMap.put(TAG_EXTENDER_MODEL, "Extender Model");
+        _tagNameMap.put(TAG_EXTENDER_FIRMWARE_VERSION, "Extender Firmware Version");
+        _tagNameMap.put(TAG_CONVERSION_LENS, "Conversion Lens");
+        _tagNameMap.put(TAG_FLASH_TYPE, "Flash Type");
+        _tagNameMap.put(TAG_FLASH_MODEL, "Flash Model");
+        _tagNameMap.put(TAG_FLASH_FIRMWARE_VERSION, "Flash Firmware Version");
+        _tagNameMap.put(TAG_FLASH_SERIAL_NUMBER, "Flash Serial Number");
+    }
+
+    public OlympusEquipmentMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusEquipmentMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Equipment";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java
new file mode 100644
index 0000000..b219b1d
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusFocusInfoMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusFocusInfoMakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusFocusInfoMakernoteDescriptor extends TagDescriptor<OlympusFocusInfoMakernoteDirectory>
+{
+    public OlympusFocusInfoMakernoteDescriptor(@NotNull OlympusFocusInfoMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagFocusInfoVersion:
+                return getFocusInfoVersionDescription();
+            case TagAutoFocus:
+                return getAutoFocusDescription();
+            case TagFocusDistance:
+                return getFocusDistanceDescription();
+            case TagAfPoint:
+                return getAfPointDescription();
+            case TagExternalFlash:
+                return getExternalFlashDescription();
+            case TagExternalFlashBounce:
+                return getExternalFlashBounceDescription();
+            case TagExternalFlashZoom:
+                return getExternalFlashZoomDescription();
+            case TagManualFlash:
+                return getManualFlashDescription();
+            case TagMacroLed:
+                return getMacroLedDescription();
+            case TagSensorTemperature:
+                return getSensorTemperatureDescription();
+            case TagImageStabilization:
+                return getImageStabilizationDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFocusInfoVersionDescription()
+    {
+        return getVersionBytesDescription(TagFocusInfoVersion, 4);
+    }
+
+    @Nullable
+    public String getAutoFocusDescription()
+    {
+        return getIndexedDescription(TagAutoFocus,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getFocusDistanceDescription()
+    {
+        Rational value = _directory.getRational(TagFocusDistance);
+        if (value == null)
+            return "inf";
+        if (value.getNumerator() == 0xFFFFFFFFL || value.getNumerator() == 0x00000000L)
+            return "inf";
+
+        return value.getNumerator() / 1000.0 + " m";
+    }
+
+    @Nullable
+    public String getAfPointDescription()
+    {
+        Integer value = _directory.getInteger(TagAfPoint);
+        if (value == null)
+            return null;
+
+        return value.toString();
+    }
+
+    @Nullable
+    public String getExternalFlashDescription()
+    {
+        int[] values = _directory.getIntArray(TagExternalFlash);
+        if (values == null || values.length < 2)
+            return null;
+
+        String join = String.format("%d %d", (short)values[0], (short)values[1]);
+
+        if(join.equals("0 0"))
+            return "Off";
+        else if(join.equals("1 0"))
+            return "On";
+        else
+            return "Unknown (" + join + ")";
+    }
+
+    @Nullable
+    public String getExternalFlashBounceDescription()
+    {
+        return getIndexedDescription(TagExternalFlashBounce,
+                "Bounce or Off", "Direct");
+    }
+
+    @Nullable
+    public String getExternalFlashZoomDescription()
+    {
+        int[] values = _directory.getIntArray(TagExternalFlashZoom);
+        if (values == null)
+        {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagExternalFlashZoom);
+            if(value == null)
+                return null;
+
+            values = new int[1];
+            values[0] = value;
+        }
+
+        if (values.length == 0)
+            return null;
+
+        String join = String.format("%d", (short)values[0]);
+        if(values.length > 1)
+            join += " " + String.format("%d", (short)values[1]);
+
+        if(join.equals("0"))
+            return "Off";
+        else if(join.equals("1"))
+            return "On";
+        else if(join.equals("0 0"))
+            return "Off";
+        else if(join.equals("1 0"))
+            return "On";
+        else
+            return "Unknown (" + join + ")";
+
+    }
+
+    @Nullable
+    public String getManualFlashDescription()
+    {
+        int[] values = _directory.getIntArray(TagManualFlash);
+        if (values == null)
+            return null;
+
+        if ((short)values[0] == 0)
+            return "Off";
+
+        if ((short)values[1] == 1)
+            return "Full";
+        return "On (1/" + (short)values[1] + " strength)";
+    }
+
+    @Nullable
+    public String getMacroLedDescription()
+    {
+        return getIndexedDescription(TagMacroLed,
+                "Off", "On");
+    }
+
+    /// <remarks>
+    /// <para>TODO: Complete when Camera Model is available.</para>
+    /// <para>There are differences in how to interpret this tag that can only be reconciled by knowing the model.</para>
+    /// </remarks>
+    @Nullable
+    public String getSensorTemperatureDescription()
+    {
+        return _directory.getString(TagSensorTemperature);
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        byte[] values = _directory.getByteArray(TagImageStabilization);
+        if (values == null)
+            return null;
+
+        if((values[0] | values[1] | values[2] | values[3]) == 0x0)
+            return "Off";
+        return "On, " + ((values[43] & 1) > 0 ? "Mode 1" : "Mode 2");
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java
new file mode 100644
index 0000000..898ae3d
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * The Olympus focus info makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusFocusInfoMakernoteDirectory extends Directory
+{
+    public static final int TagFocusInfoVersion = 0x0000;
+    public static final int TagAutoFocus = 0x0209;
+    public static final int TagSceneDetect = 0x0210;
+    public static final int TagSceneArea = 0x0211;
+    public static final int TagSceneDetectData = 0x0212;
+
+    public static final int TagZoomStepCount = 0x0300;
+    public static final int TagFocusStepCount = 0x0301;
+    public static final int TagFocusStepInfinity = 0x0303;
+    public static final int TagFocusStepNear = 0x0304;
+    public static final int TagFocusDistance = 0x0305;
+    public static final int TagAfPoint = 0x0308;
+    // 0x031a Continuous AF parameters?
+    public static final int TagAfInfo = 0x0328;    // ifd
+
+    public static final int TagExternalFlash = 0x1201;
+    public static final int TagExternalFlashGuideNumber = 0x1203;
+    public static final int TagExternalFlashBounce = 0x1204;
+    public static final int TagExternalFlashZoom = 0x1205;
+    public static final int TagInternalFlash = 0x1208;
+    public static final int TagManualFlash = 0x1209;
+    public static final int TagMacroLed = 0x120A;
+
+    public static final int TagSensorTemperature = 0x1500;
+
+    public static final int TagImageStabilization = 0x1600;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagFocusInfoVersion, "Focus Info Version");
+        _tagNameMap.put(TagAutoFocus, "Auto Focus");
+        _tagNameMap.put(TagSceneDetect, "Scene Detect");
+        _tagNameMap.put(TagSceneArea, "Scene Area");
+        _tagNameMap.put(TagSceneDetectData, "Scene Detect Data");
+        _tagNameMap.put(TagZoomStepCount, "Zoom Step Count");
+        _tagNameMap.put(TagFocusStepCount, "Focus Step Count");
+        _tagNameMap.put(TagFocusStepInfinity, "Focus Step Infinity");
+        _tagNameMap.put(TagFocusStepNear, "Focus Step Near");
+        _tagNameMap.put(TagFocusDistance, "Focus Distance");
+        _tagNameMap.put(TagAfPoint, "AF Point");
+        _tagNameMap.put(TagAfInfo, "AF Info");
+        _tagNameMap.put(TagExternalFlash, "External Flash");
+        _tagNameMap.put(TagExternalFlashGuideNumber, "External Flash Guide Number");
+        _tagNameMap.put(TagExternalFlashBounce, "External Flash Bounce");
+        _tagNameMap.put(TagExternalFlashZoom, "External Flash Zoom");
+        _tagNameMap.put(TagInternalFlash, "Internal Flash");
+        _tagNameMap.put(TagManualFlash, "Manual Flash");
+        _tagNameMap.put(TagMacroLed, "Macro LED");
+        _tagNameMap.put(TagSensorTemperature, "Sensor Temperature");
+        _tagNameMap.put(TagImageStabilization, "Image Stabilization");
+    }
+
+    public OlympusFocusInfoMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusFocusInfoMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Focus Info";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java
new file mode 100644
index 0000000..c2add9a
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.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.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusImageProcessingMakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.33 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusImageProcessingMakernoteDescriptor extends TagDescriptor<OlympusImageProcessingMakernoteDirectory>
+{
+    public OlympusImageProcessingMakernoteDescriptor(@NotNull OlympusImageProcessingMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagImageProcessingVersion:
+                return getImageProcessingVersionDescription();
+            case TagColorMatrix:
+                return getColorMatrixDescription();
+            case TagNoiseReduction2:
+                return getNoiseReduction2Description();
+            case TagDistortionCorrection2:
+                return getDistortionCorrection2Description();
+            case TagShadingCompensation2:
+                return getShadingCompensation2Description();
+            case TagMultipleExposureMode:
+                return getMultipleExposureModeDescription();
+            case TagAspectRatio:
+                return getAspectRatioDescription();
+            case TagKeystoneCompensation:
+                return getKeystoneCompensationDescription();
+            case TagKeystoneDirection:
+                return getKeystoneDirectionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getImageProcessingVersionDescription()
+    {
+        return getVersionBytesDescription(TagImageProcessingVersion, 4);
+    }
+
+    @Nullable
+    public String getColorMatrixDescription()
+    {
+        int[] obj = _directory.getIntArray(TagColorMatrix);
+        if (obj == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < obj.length; i++) {
+            if (i != 0)
+                sb.append(" ");
+            sb.append((short)obj[i]);
+        }
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getNoiseReduction2Description()
+    {
+        Integer value = _directory.getInteger(TagNoiseReduction2);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        short v = value.shortValue();
+
+        if (( v       & 1) != 0) sb.append("Noise Reduction, ");
+        if (((v >> 1) & 1) != 0) sb.append("Noise Filter, ");
+        if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getDistortionCorrection2Description()
+    {
+        return getIndexedDescription(TagDistortionCorrection2, "Off", "On");
+    }
+
+    @Nullable
+    public String getShadingCompensation2Description()
+    {
+        return getIndexedDescription(TagShadingCompensation2, "Off", "On");
+    }
+
+    @Nullable
+    public String getMultipleExposureModeDescription()
+    {
+        int[] values = _directory.getIntArray(TagMultipleExposureMode);
+        if (values == null)
+        {
+            // check if it's only one value long also
+            Integer value = _directory.getInteger(TagMultipleExposureMode);
+            if(value == null)
+                return null;
+
+            values = new int[1];
+            values[0] = value;
+        }
+
+        if (values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        switch ((short)values[0])
+        {
+            case 0:
+                sb.append("Off");
+                break;
+            case 2:
+                sb.append("On (2 frames)");
+                break;
+            case 3:
+                sb.append("On (3 frames)");
+                break;
+            default:
+                sb.append("Unknown (").append((short)values[0]).append(")");
+                break;
+        }
+
+        if (values.length > 1)
+            sb.append("; ").append((short)values[1]);
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getAspectRatioDescription()
+    {
+        byte[] values = _directory.getByteArray(TagAspectRatio);
+        if (values == null || values.length < 2)
+            return null;
+
+        String join = String.format("%d %d", values[0], values[1]);
+
+        String ret;
+        if(join.equals("1 1"))
+            ret = "4:3";
+        else if(join.equals("1 4"))
+            ret = "1:1";
+        else if(join.equals("2 1"))
+            ret = "3:2 (RAW)";
+        else if(join.equals("2 2"))
+            ret = "3:2";
+        else if(join.equals("3 1"))
+            ret = "16:9 (RAW)";
+        else if(join.equals("3 3"))
+            ret = "16:9";
+        else if(join.equals("4 1"))
+            ret = "1:1 (RAW)";
+        else if(join.equals("4 4"))
+            ret = "6:6";
+        else if(join.equals("5 5"))
+            ret = "5:4";
+        else if(join.equals("6 6"))
+            ret = "7:6";
+        else if(join.equals("7 7"))
+            ret = "6:5";
+        else if(join.equals("8 8"))
+            ret = "7:5";
+        else if(join.equals("9 1"))
+            ret = "3:4 (RAW)";
+        else if(join.equals("9 9"))
+            ret = "3:4";
+        else
+            ret = "Unknown (" + join + ")";
+
+        return ret;
+    }
+
+    @Nullable
+    public String getKeystoneCompensationDescription()
+    {
+        byte[] values = _directory.getByteArray(TagKeystoneCompensation);
+        if (values == null || values.length < 2)
+            return null;
+
+        String join = String.format("%d %d", values[0], values[1]);
+
+        String ret;
+        if(join.equals("0 0"))
+            ret = "Off";
+        else if(join.equals("0 1"))
+            ret = "On";
+        else
+            ret = "Unknown (" + join + ")";
+
+        return ret;
+    }
+
+    @Nullable
+    public String getKeystoneDirectionDescription()
+    {
+        return getIndexedDescription(TagKeystoneDirection, "Vertical", "Horizontal");
+    }
+}
\ No newline at end of file
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java
new file mode 100644
index 0000000..4452584
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * The Olympus image processing makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusImageProcessingMakernoteDirectory extends Directory
+{
+    public static final int TagImageProcessingVersion = 0x0000;
+    public static final int TagWbRbLevels = 0x0100;
+    // 0x0101 - in-camera AutoWB unless it is all 0's or all 256's (ref IB)
+    public static final int TagWbRbLevels3000K = 0x0102;
+    public static final int TagWbRbLevels3300K = 0x0103;
+    public static final int TagWbRbLevels3600K = 0x0104;
+    public static final int TagWbRbLevels3900K = 0x0105;
+    public static final int TagWbRbLevels4000K = 0x0106;
+    public static final int TagWbRbLevels4300K = 0x0107;
+    public static final int TagWbRbLevels4500K = 0x0108;
+    public static final int TagWbRbLevels4800K = 0x0109;
+    public static final int TagWbRbLevels5300K = 0x010a;
+    public static final int TagWbRbLevels6000K = 0x010b;
+    public static final int TagWbRbLevels6600K = 0x010c;
+    public static final int TagWbRbLevels7500K = 0x010d;
+    public static final int TagWbRbLevelsCwB1 = 0x010e;
+    public static final int TagWbRbLevelsCwB2 = 0x010f;
+    public static final int TagWbRbLevelsCwB3 = 0x0110;
+    public static final int TagWbRbLevelsCwB4 = 0x0111;
+    public static final int TagWbGLevel3000K = 0x0113;
+    public static final int TagWbGLevel3300K = 0x0114;
+    public static final int TagWbGLevel3600K = 0x0115;
+    public static final int TagWbGLevel3900K = 0x0116;
+    public static final int TagWbGLevel4000K = 0x0117;
+    public static final int TagWbGLevel4300K = 0x0118;
+    public static final int TagWbGLevel4500K = 0x0119;
+    public static final int TagWbGLevel4800K = 0x011a;
+    public static final int TagWbGLevel5300K = 0x011b;
+    public static final int TagWbGLevel6000K = 0x011c;
+    public static final int TagWbGLevel6600K = 0x011d;
+    public static final int TagWbGLevel7500K = 0x011e;
+    public static final int TagWbGLevel = 0x011f;
+    // 0x0121 = WB preset for flash (about 6000K) (ref IB)
+    // 0x0125 = WB preset for underwater (ref IB)
+
+    public static final int TagColorMatrix = 0x0200;
+    // color matrices (ref 11):
+    // 0x0201-0x020d are sRGB color matrices
+    // 0x020e-0x021a are Adobe RGB color matrices
+    // 0x021b-0x0227 are ProPhoto RGB color matrices
+    // 0x0228 and 0x0229 are ColorMatrix for E-330
+    // 0x0250-0x0252 are sRGB color matrices
+    // 0x0253-0x0255 are Adobe RGB color matrices
+    // 0x0256-0x0258 are ProPhoto RGB color matrices
+
+    public static final int TagEnhancer = 0x0300;
+    public static final int TagEnhancerValues = 0x0301;
+    public static final int TagCoringFilter = 0x0310;
+    public static final int TagCoringValues = 0x0311;
+    public static final int TagBlackLevel2 = 0x0600;
+    public static final int TagGainBase = 0x0610;
+    public static final int TagValidBits = 0x0611;
+    public static final int TagCropLeft = 0x0612;
+    public static final int TagCropTop = 0x0613;
+    public static final int TagCropWidth = 0x0614;
+    public static final int TagCropHeight = 0x0615;
+    public static final int TagUnknownBlock1 = 0x0635;
+    public static final int TagUnknownBlock2 = 0x0636;
+
+    // 0x0800 LensDistortionParams, float[9] (ref 11)
+    // 0x0801 LensShadingParams, int16u[16] (ref 11)
+    public static final int TagSensorCalibration = 0x0805;
+
+    public static final int TagNoiseReduction2 = 0x1010;
+    public static final int TagDistortionCorrection2 = 0x1011;
+    public static final int TagShadingCompensation2 = 0x1012;
+    public static final int TagMultipleExposureMode = 0x101c;
+    public static final int TagUnknownBlock3 = 0x1103;
+    public static final int TagUnknownBlock4 = 0x1104;
+    public static final int TagAspectRatio = 0x1112;
+    public static final int TagAspectFrame = 0x1113;
+    public static final int TagFacesDetected = 0x1200;
+    public static final int TagFaceDetectArea = 0x1201;
+    public static final int TagMaxFaces = 0x1202;
+    public static final int TagFaceDetectFrameSize = 0x1203;
+    public static final int TagFaceDetectFrameCrop = 0x1207;
+    public static final int TagCameraTemperature = 0x1306;
+
+    public static final int TagKeystoneCompensation = 0x1900;
+    public static final int TagKeystoneDirection = 0x1901;
+    // 0x1905 - focal length (PH, E-M1)
+    public static final int TagKeystoneValue = 0x1906;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagImageProcessingVersion, "Image Processing Version");
+        _tagNameMap.put(TagWbRbLevels, "WB RB Levels");
+        _tagNameMap.put(TagWbRbLevels3000K, "WB RB Levels 3000K");
+        _tagNameMap.put(TagWbRbLevels3300K, "WB RB Levels 3300K");
+        _tagNameMap.put(TagWbRbLevels3600K, "WB RB Levels 3600K");
+        _tagNameMap.put(TagWbRbLevels3900K, "WB RB Levels 3900K");
+        _tagNameMap.put(TagWbRbLevels4000K, "WB RB Levels 4000K");
+        _tagNameMap.put(TagWbRbLevels4300K, "WB RB Levels 4300K");
+        _tagNameMap.put(TagWbRbLevels4500K, "WB RB Levels 4500K");
+        _tagNameMap.put(TagWbRbLevels4800K, "WB RB Levels 4800K");
+        _tagNameMap.put(TagWbRbLevels5300K, "WB RB Levels 5300K");
+        _tagNameMap.put(TagWbRbLevels6000K, "WB RB Levels 6000K");
+        _tagNameMap.put(TagWbRbLevels6600K, "WB RB Levels 6600K");
+        _tagNameMap.put(TagWbRbLevels7500K, "WB RB Levels 7500K");
+        _tagNameMap.put(TagWbRbLevelsCwB1, "WB RB Levels CWB1");
+        _tagNameMap.put(TagWbRbLevelsCwB2, "WB RB Levels CWB2");
+        _tagNameMap.put(TagWbRbLevelsCwB3, "WB RB Levels CWB3");
+        _tagNameMap.put(TagWbRbLevelsCwB4, "WB RB Levels CWB4");
+        _tagNameMap.put(TagWbGLevel3000K, "WB G Level 3000K");
+        _tagNameMap.put(TagWbGLevel3300K, "WB G Level 3300K");
+        _tagNameMap.put(TagWbGLevel3600K, "WB G Level 3600K");
+        _tagNameMap.put(TagWbGLevel3900K, "WB G Level 3900K");
+        _tagNameMap.put(TagWbGLevel4000K, "WB G Level 4000K");
+        _tagNameMap.put(TagWbGLevel4300K, "WB G Level 4300K");
+        _tagNameMap.put(TagWbGLevel4500K, "WB G Level 4500K");
+        _tagNameMap.put(TagWbGLevel4800K, "WB G Level 4800K");
+        _tagNameMap.put(TagWbGLevel5300K, "WB G Level 5300K");
+        _tagNameMap.put(TagWbGLevel6000K, "WB G Level 6000K");
+        _tagNameMap.put(TagWbGLevel6600K, "WB G Level 6600K");
+        _tagNameMap.put(TagWbGLevel7500K, "WB G Level 7500K");
+        _tagNameMap.put(TagWbGLevel, "WB G Level");
+
+        _tagNameMap.put(TagColorMatrix, "Color Matrix");
+
+        _tagNameMap.put(TagEnhancer, "Enhancer");
+        _tagNameMap.put(TagEnhancerValues, "Enhancer Values");
+        _tagNameMap.put(TagCoringFilter, "Coring Filter");
+        _tagNameMap.put(TagCoringValues, "Coring Values");
+        _tagNameMap.put(TagBlackLevel2, "Black Level 2");
+        _tagNameMap.put(TagGainBase, "Gain Base");
+        _tagNameMap.put(TagValidBits, "Valid Bits");
+        _tagNameMap.put(TagCropLeft, "Crop Left");
+        _tagNameMap.put(TagCropTop, "Crop Top");
+        _tagNameMap.put(TagCropWidth, "Crop Width");
+        _tagNameMap.put(TagCropHeight, "Crop Height");
+        _tagNameMap.put(TagUnknownBlock1, "Unknown Block 1");
+        _tagNameMap.put(TagUnknownBlock2, "Unknown Block 2");
+
+        _tagNameMap.put(TagSensorCalibration, "Sensor Calibration");
+
+        _tagNameMap.put(TagNoiseReduction2, "Noise Reduction 2");
+        _tagNameMap.put(TagDistortionCorrection2, "Distortion Correction 2");
+        _tagNameMap.put(TagShadingCompensation2, "Shading Compensation 2");
+        _tagNameMap.put(TagMultipleExposureMode, "Multiple Exposure Mode");
+        _tagNameMap.put(TagUnknownBlock3, "Unknown Block 3");
+        _tagNameMap.put(TagUnknownBlock4, "Unknown Block 4");
+        _tagNameMap.put(TagAspectRatio, "Aspect Ratio");
+        _tagNameMap.put(TagAspectFrame, "Aspect Frame");
+        _tagNameMap.put(TagFacesDetected, "Faces Detected");
+        _tagNameMap.put(TagFaceDetectArea, "Face Detect Area");
+        _tagNameMap.put(TagMaxFaces, "Max Faces");
+        _tagNameMap.put(TagFaceDetectFrameSize, "Face Detect Frame Size");
+        _tagNameMap.put(TagFaceDetectFrameCrop, "Face Detect Frame Crop");
+        _tagNameMap.put(TagCameraTemperature , "Camera Temperature");
+        _tagNameMap.put(TagKeystoneCompensation, "Keystone Compensation");
+        _tagNameMap.put(TagKeystoneDirection, "Keystone Direction");
+        _tagNameMap.put(TagKeystoneValue, "Keystone Value");
+    }
+
+    public OlympusImageProcessingMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusImageProcessingMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Image Processing";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
index f003fcb..41a313d 100644
--- a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -20,11 +20,15 @@
  */
 package com.drew.metadata.exif.makernotes;
 
+import com.drew.imaging.PhotographicConversions;
+import com.drew.lang.Rational;
+import com.drew.lang.DateUtil;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
-import java.util.GregorianCalendar;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
 
 import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*;
 
@@ -33,6 +37,7 @@ import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 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
@@ -63,10 +68,22 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
                 return getMacroModeDescription();
             case TAG_BW_MODE:
                 return getBWModeDescription();
-            case TAG_DIGI_ZOOM_RATIO:
-                return getDigiZoomRatioDescription();
+            case TAG_DIGITAL_ZOOM:
+                return getDigitalZoomDescription();
+            case TAG_FOCAL_PLANE_DIAGONAL:
+                return getFocalPlaneDiagonalDescription();
+            case TAG_CAMERA_TYPE:
+                return getCameraTypeDescription();
             case TAG_CAMERA_ID:
                 return getCameraIdDescription();
+            case TAG_ONE_TOUCH_WB:
+                return getOneTouchWbDescription();
+            case TAG_SHUTTER_SPEED_VALUE:
+                return getShutterSpeedDescription();
+            case TAG_ISO_VALUE:
+                return getIsoValueDescription();
+            case TAG_APERTURE_VALUE:
+                return getApertureValueDescription();
             case TAG_FLASH_MODE:
                 return getFlashModeDescription();
             case TAG_FOCUS_RANGE:
@@ -75,6 +92,18 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
                 return getFocusModeDescription();
             case TAG_SHARPNESS:
                 return getSharpnessDescription();
+            case TAG_COLOUR_MATRIX:
+                return getColorMatrixDescription();
+            case TAG_WB_MODE:
+                return getWbModeDescription();
+            case TAG_RED_BALANCE:
+                return getRedBalanceDescription();
+            case TAG_BLUE_BALANCE:
+                return getBlueBalanceDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_PREVIEW_IMAGE_VALID:
+                return getPreviewImageValidDescription();
 
             case CameraSettings.TAG_EXPOSURE_MODE:
                 return getExposureModeDescription();
@@ -99,7 +128,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
             case CameraSettings.TAG_MACRO_MODE:
                 return getMacroModeCameraSettingDescription();
             case CameraSettings.TAG_DIGITAL_ZOOM:
-                return getDigitalZoomDescription();
+                return getDigitalZoomCameraSettingDescription();
             case CameraSettings.TAG_EXPOSURE_COMPENSATION:
                 return getExposureCompensationDescription();
             case CameraSettings.TAG_BRACKET_STEP:
@@ -114,7 +143,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
             case CameraSettings.TAG_FOCUS_DISTANCE:
                 return getFocusDistanceDescription();
             case CameraSettings.TAG_FLASH_FIRED:
-                return getFlastFiredDescription();
+                return getFlashFiredDescription();
             case CameraSettings.TAG_DATE:
                 return getDateDescription();
             case CameraSettings.TAG_TIME:
@@ -135,13 +164,13 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
             case CameraSettings.TAG_SATURATION:
                 return getSaturationDescription();
             case CameraSettings.TAG_CONTRAST:
-                return getContrastDescription();
+                return getContrastCameraSettingDescription();
             case CameraSettings.TAG_SHARPNESS:
                 return getSharpnessCameraSettingDescription();
             case CameraSettings.TAG_SUBJECT_PROGRAM:
                 return getSubjectProgramDescription();
             case CameraSettings.TAG_FLASH_COMPENSATION:
-                return getFlastCompensationDescription();
+                return getFlashCompensationDescription();
             case CameraSettings.TAG_ISO_SETTING:
                 return getIsoSettingDescription();
             case CameraSettings.TAG_CAMERA_MODEL:
@@ -257,7 +286,9 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
             return null;
 
         double iso = Math.pow((value / 8d) - 1, 2) * 3.125;
-        return Double.toString(iso);
+        DecimalFormat format = new DecimalFormat("0.##");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return format.format(iso);
     }
 
     @Nullable
@@ -273,7 +304,9 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
             return null;
 
         double shutterSpeed = Math.pow((49-value) / 8d, 2);
-        return Double.toString(shutterSpeed) + " sec";
+        DecimalFormat format = new DecimalFormat("0.###");
+        format.setRoundingMode(RoundingMode.HALF_UP);
+        return format.format(shutterSpeed) + " sec";
     }
 
     @Nullable
@@ -288,7 +321,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
             return null;
 
         double fStop = Math.pow((value/16d) - 0.5, 2);
-        return "F" + Double.toString(fStop);
+        return getFStopDescription(fStop);
     }
 
     @Nullable
@@ -298,7 +331,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     }
 
     @Nullable
-    public String getDigitalZoomDescription()
+    public String getDigitalZoomCameraSettingDescription()
     {
         return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "Off", "Electronic magnification", "Digital zoom 2x");
     }
@@ -307,7 +340,8 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     public String getExposureCompensationDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_EXPOSURE_COMPENSATION);
-        return value == null ? null : ((value / 3d) - 2) + " EV";
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format((value / 3d) - 2) + " EV";
     }
 
     @Nullable
@@ -340,7 +374,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     public String getFocalLengthDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_FOCAL_LENGTH);
-        return value == null ? null : Double.toString(value/256d) + " mm";
+        return value == null ? null : getFocalLengthDescription(value/256d);
     }
 
     @Nullable
@@ -355,7 +389,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     }
 
     @Nullable
-    public String getFlastFiredDescription()
+    public String getFlashFiredDescription()
     {
         return getIndexedDescription(CameraSettings.TAG_FLASH_FIRED, "No", "Yes");
     }
@@ -369,10 +403,15 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
         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();
+
+        int day = (int) (value & 0xFF);
+        int month = (int) ((value >> 16) & 0xFF);
+        int year = (int) ((value >> 8) & 0xFF) + 1970;
+
+        if (!DateUtil.isValidDate(year, month, day))
+            return "Invalid date";
+
+        return String.format("%04d-%02d-%02d", year, month + 1, day);
     }
 
     @Nullable
@@ -384,9 +423,13 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
         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;
+
+        int hours = (int) ((value >> 8) & 0xFF);
+        int minutes = (int) ((value >> 16) & 0xFF);
+        int seconds = (int) (value & 0xFF);
+
+        if (!DateUtil.isValidTime(hours, minutes, seconds))
+            return "Invalid time";
 
         return String.format("%02d:%02d:%02d", hours, minutes, seconds);
     }
@@ -399,7 +442,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
         if (value == null)
             return null;
         double fStop = Math.pow((value/16d) - 0.5, 2);
-        return "F" + fStop;
+        return getFStopDescription(fStop);
     }
 
     @Nullable
@@ -423,21 +466,24 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     public String getWhiteBalanceRedDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_RED);
-        return value == null ? null : Double.toString(value/256d);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format(value/256d);
     }
 
     @Nullable
     public String getWhiteBalanceGreenDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_GREEN);
-        return value == null ? null : Double.toString(value/256d);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format(value/256d);
     }
 
     @Nullable
     public String getWhiteBalanceBlueDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_BLUE);
-        return value == null ? null : Double.toString(value/256d);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format(value / 256d);
     }
 
     @Nullable
@@ -448,7 +494,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     }
 
     @Nullable
-    public String getContrastDescription()
+    public String getContrastCameraSettingDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_CONTRAST);
         return value == null ? null : Long.toString(value-3);
@@ -467,10 +513,11 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     }
 
     @Nullable
-    public String getFlastCompensationDescription()
+    public String getFlashCompensationDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_FLASH_COMPENSATION);
-        return value == null ? null : ((value-6)/3d) + " EV";
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format((value-6)/3d) + " EV";
     }
 
     @Nullable
@@ -534,7 +581,8 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     public String getApexBrightnessDescription()
     {
         Long value = _directory.getLongObject(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE);
-        return value == null ? null : Double.toString((value/8d)-6);
+        DecimalFormat format = new DecimalFormat("0.##");
+        return value == null ? null : format.format((value/8d)-6);
     }
 
     @Nullable
@@ -624,6 +672,93 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
         return getIndexedDescription(TAG_SHARPNESS, "Normal", "Hard", "Soft");
     }
 
+    @Nullable
+    public String getColorMatrixDescription()
+    {
+        int[] obj = _directory.getIntArray(TAG_COLOUR_MATRIX);
+        if (obj == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < obj.length; i++) {
+            sb.append((short)obj[i]);
+            if (i < obj.length - 1)
+                sb.append(" ");
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    @Nullable
+    public String getWbModeDescription()
+    {
+        int[] obj = _directory.getIntArray(TAG_WB_MODE);
+        if (obj == null)
+            return null;
+
+        String val = String.format("%d %d", obj[0], obj[1]);
+
+        if(val.equals("1 0"))
+            return "Auto";
+        else if(val.equals("1 2"))
+            return "Auto (2)";
+        else if(val.equals("1 4"))
+            return "Auto (4)";
+        else if(val.equals("2 2"))
+            return "3000 Kelvin";
+        else if(val.equals("2 3"))
+            return "3700 Kelvin";
+        else if(val.equals("2 4"))
+            return "4000 Kelvin";
+        else if(val.equals("2 5"))
+            return "4500 Kelvin";
+        else if(val.equals("2 6"))
+            return "5500 Kelvin";
+        else if(val.equals("2 7"))
+            return "6500 Kelvin";
+        else if(val.equals("2 8"))
+            return "7500 Kelvin";
+        else if(val.equals("3 0"))
+            return "One-touch";
+        else
+            return "Unknown " + val;
+    }
+
+    @Nullable
+    public String getRedBalanceDescription()
+    {
+        int[] values = _directory.getIntArray(TAG_RED_BALANCE);
+        if (values == null)
+            return null;
+
+        short value = (short)values[0];
+
+        return String.valueOf((double)value/256d);
+    }
+
+    @Nullable
+    public String getBlueBalanceDescription()
+    {
+        int[] values = _directory.getIntArray(TAG_BLUE_BALANCE);
+        if (values == null)
+            return null;
+
+        short value = (short)values[0];
+
+        return String.valueOf((double)value/256d);
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST, "High", "Normal", "Low");
+    }
+
+    @Nullable
+    public String getPreviewImageValidDescription()
+    {
+        return getIndexedDescription(TAG_PREVIEW_IMAGE_VALID, "No", "Yes");
+    }
+
     @Nullable
     public String getFocusModeDescription()
     {
@@ -643,9 +778,36 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     }
 
     @Nullable
-    public String getDigiZoomRatioDescription()
+    public String getDigitalZoomDescription()
     {
-        return getIndexedDescription(TAG_DIGI_ZOOM_RATIO, "Normal", null, "Digital 2x Zoom");
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM);
+        if (value == null)
+            return null;
+        return value.toSimpleString(false);
+    }
+
+    @Nullable
+    public String getFocalPlaneDiagonalDescription()
+    {
+        Rational value = _directory.getRational(TAG_FOCAL_PLANE_DIAGONAL);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.###");
+        return format.format(value.doubleValue()) + " mm";
+    }
+
+    @Nullable
+    public String getCameraTypeDescription()
+    {
+        String cameratype = _directory.getString(TAG_CAMERA_TYPE);
+        if(cameratype == null)
+            return null;
+
+        if(OlympusMakernoteDirectory.OlympusCameraTypes.containsKey(cameratype))
+            return OlympusMakernoteDirectory.OlympusCameraTypes.get(cameratype);
+
+        return cameratype;
     }
 
     @Nullable
@@ -657,6 +819,38 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
         return new String(bytes);
     }
 
+    @Nullable
+    public String getOneTouchWbDescription()
+    {
+        return getIndexedDescription(TAG_ONE_TOUCH_WB, "Off", "On", "On (Preset)");
+    }
+
+    @Nullable
+    public String getShutterSpeedDescription()
+    {
+        return super.getShutterSpeedDescription(TAG_SHUTTER_SPEED_VALUE);
+    }
+
+    @Nullable
+    public String getIsoValueDescription()
+    {
+        Rational value = _directory.getRational(TAG_ISO_VALUE);
+        if (value == null)
+            return null;
+
+        return String.valueOf(Math.round(Math.pow(2, value.doubleValue() - 5) * 100));
+    }
+
+    @Nullable
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE_VALUE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return getFStopDescription(fStop);
+    }
+
     @Nullable
     public String getMacroModeDescription()
     {
@@ -672,7 +866,56 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi
     @Nullable
     public String getJpegQualityDescription()
     {
-        return getIndexedDescription(TAG_JPEG_QUALITY,
+        String cameratype = _directory.getString(TAG_CAMERA_TYPE);
+
+        if(cameratype != null)
+        {
+            Integer value = _directory.getInteger(TAG_JPEG_QUALITY);
+            if(value == null)
+                return null;
+
+            if((cameratype.startsWith("SX") && !cameratype.startsWith("SX151"))
+                || cameratype.startsWith("D4322"))
+            {
+                switch (value)
+                {
+                    case 0:
+                        return "Standard Quality (Low)";
+                    case 1:
+                        return "High Quality (Normal)";
+                    case 2:
+                        return "Super High Quality (Fine)";
+                    case 6:
+                        return "RAW";
+                    default:
+                        return "Unknown (" + value.toString() + ")";
+                }
+            }
+            else
+            {
+                switch (value)
+                {
+                    case 0:
+                        return "Standard Quality (Low)";
+                    case 1:
+                        return "High Quality (Normal)";
+                    case 2:
+                        return "Super High Quality (Fine)";
+                    case 4:
+                        return "RAW";
+                    case 5:
+                        return "Medium-Fine";
+                    case 6:
+                        return "Small-Fine";
+                    case 33:
+                        return "Uncompressed";
+                    default:
+                        return "Unknown (" + value.toString() + ")";
+                }
+            }
+        }
+        else
+            return getIndexedDescription(TAG_JPEG_QUALITY,
             1,
             "Standard Quality",
             "High Quality",
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
index dbfa316..2856a66 100644
--- a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class OlympusMakernoteDirectory extends Directory
 {
     /** Used by Konica / Minolta cameras. */
@@ -50,6 +51,8 @@ public class OlympusMakernoteDirectory extends Directory
     /** 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
@@ -82,6 +85,7 @@ public class OlympusMakernoteDirectory extends Directory
      */
     public static final int TAG_IMAGE_QUALITY_2 = 0x0103;
 
+    public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104;
 
     /**
      * Three values:
@@ -113,10 +117,10 @@ public class OlympusMakernoteDirectory extends Directory
     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_DIGITAL_ZOOM = 0x0204;
     public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0205;
     public static final int TAG_LENS_DISTORTION_PARAMETERS = 0x0206;
-    public static final int TAG_FIRMWARE_VERSION = 0x0207;
+    public static final int TAG_CAMERA_TYPE = 0x0207;
     public static final int TAG_PICT_INFO = 0x0208;
     public static final int TAG_CAMERA_ID = 0x0209;
 
@@ -135,41 +139,99 @@ public class OlympusMakernoteDirectory extends Directory
     /** 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_SERIAL_NUMBER_1 = 0x0404;
+    public static final int TAG_FIRMWARE = 0x0405;
+
     /**
      * See the PIM specification here:
      * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
      */
     public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
-    public static final int TAG_DATA_DUMP = 0x0F00;
+    public static final int TAG_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_WHITE_BALANCE = 0x1015;
-    public static final int TAG_RED_BIAS = 0x1017;
-    public static final int TAG_BLUE_BIAS = 0x1018;
-    public static final int TAG_SERIAL_NUMBER = 0x101A;
+    public static final int TAG_COLOR_TEMPERATURE_BG = 0x1013;
+    public static final int TAG_COLOR_TEMPERATURE_RG = 0x1014;
+    public static final int TAG_WB_MODE = 0x1015;
+//    public static final int TAG_ = 0x1016;
+    public static final int TAG_RED_BALANCE = 0x1017;
+    public static final int TAG_BLUE_BALANCE = 0x1018;
+    public static final int TAG_COLOR_MATRIX_NUMBER = 0x1019;
+    public static final int TAG_SERIAL_NUMBER_2 = 0x101A;
+
+    public static final int TAG_EXTERNAL_FLASH_AE1_0 = 0x101B;
+    public static final int TAG_EXTERNAL_FLASH_AE2_0 = 0x101C;
+    public static final int TAG_INTERNAL_FLASH_AE1_0 = 0x101D;
+    public static final int TAG_INTERNAL_FLASH_AE2_0 = 0x101E;
+    public static final int TAG_EXTERNAL_FLASH_AE1 = 0x101F;
+    public static final int TAG_EXTERNAL_FLASH_AE2 = 0x1020;
+    public static final int TAG_INTERNAL_FLASH_AE1 = 0x1021;
+    public static final int TAG_INTERNAL_FLASH_AE2 = 0x1022;
+
     public static final int TAG_FLASH_BIAS = 0x1023;
+    public static final int TAG_INTERNAL_FLASH_TABLE = 0x1024;
+    public static final int TAG_EXTERNAL_FLASH_G_VALUE = 0x1025;
+    public static final int TAG_EXTERNAL_FLASH_BOUNCE = 0x1026;
+    public static final int TAG_EXTERNAL_FLASH_ZOOM = 0x1027;
+    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_OLYMPUS_IMAGE_WIDTH = 0x102E;
+    public static final int TAG_OLYMPUS_IMAGE_HEIGHT = 0x102F;
+    public static final int TAG_SCENE_DETECT = 0x1030;
+    public static final int TAG_SCENE_AREA = 0x1031;
+//    public static final int TAG_ = 0x1032;
+    public static final int TAG_SCENE_DETECT_DATA = 0x1033;
     public static final int TAG_COMPRESSION_RATIO = 0x1034;
+    public static final int TAG_PREVIEW_IMAGE_VALID = 0x1035;
+    public static final int TAG_PREVIEW_IMAGE_START = 0x1036;
+    public static final int TAG_PREVIEW_IMAGE_LENGTH = 0x1037;
+    public static final int TAG_AF_RESULT = 0x1038;
+    public static final int TAG_CCD_SCAN_MODE = 0x1039;
+    public static final int TAG_NOISE_REDUCTION = 0x103A;
+    public static final int TAG_INFINITY_LENS_STEP = 0x103B;
+    public static final int TAG_NEAR_LENS_STEP = 0x103C;
+    public static final int TAG_LIGHT_VALUE_CENTER = 0x103D;
+    public static final int TAG_LIGHT_VALUE_PERIPHERY = 0x103E;
+    public static final int TAG_FIELD_COUNT = 0x103F;
+    public static final int TAG_EQUIPMENT = 0x2010;
+    public static final int TAG_CAMERA_SETTINGS = 0x2020;
+    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 static final int TAG_MAIN_INFO = 0x4000;
 
     public final static class CameraSettings
     {
@@ -190,7 +252,7 @@ public class OlympusMakernoteDirectory extends Directory
         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;
@@ -199,7 +261,7 @@ public class OlympusMakernoteDirectory extends Directory
         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;
@@ -231,17 +293,6 @@ public class OlympusMakernoteDirectory extends Directory
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static {
-        _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode");
-        _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality");
-        _tagNameMap.put(TAG_MACRO_MODE, "Macro");
-        _tagNameMap.put(TAG_BW_MODE, "BW Mode");
-        _tagNameMap.put(TAG_DIGI_ZOOM_RATIO, "DigiZoom Ratio");
-        _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
-        _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters");
-        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
-        _tagNameMap.put(TAG_PICT_INFO, "Pict Info");
-        _tagNameMap.put(TAG_CAMERA_ID, "Camera Id");
-        _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
         _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
         _tagNameMap.put(TAG_CAMERA_SETTINGS_1, "Camera Settings");
         _tagNameMap.put(TAG_CAMERA_SETTINGS_2, "Camera Settings");
@@ -249,41 +300,106 @@ public class OlympusMakernoteDirectory extends Directory
         _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_IMAGE_HEIGHT, "Image Height");
+        _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_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal");
+        _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters");
+        _tagNameMap.put(TAG_CAMERA_TYPE, "Camera Type");
+        _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_SERIAL_NUMBER_1, "Serial Number");
+        _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_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE_BG, "Color Temperature BG");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE_RG, "Color Temperature RG");
+        _tagNameMap.put(TAG_WB_MODE, "White Balance Mode");
+        _tagNameMap.put(TAG_RED_BALANCE, "Red Balance");
+        _tagNameMap.put(TAG_BLUE_BALANCE, "Blue Balance");
+        _tagNameMap.put(TAG_COLOR_MATRIX_NUMBER, "Color Matrix Number");
+        _tagNameMap.put(TAG_SERIAL_NUMBER_2, "Serial Number");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE1_0, "External Flash AE1 0");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE2_0, "External Flash AE2 0");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE1_0, "Internal Flash AE1 0");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE2_0, "Internal Flash AE2 0");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE1, "External Flash AE1");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_AE2, "External Flash AE2");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE1, "Internal Flash AE1");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_AE2, "Internal Flash AE2");
         _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_INTERNAL_FLASH_TABLE, "Internal Flash Table");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_G_VALUE, "External Flash G Value");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_BOUNCE, "External Flash Bounce");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_ZOOM, "External Flash Zoom");
+        _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_OLYMPUS_IMAGE_WIDTH, "Olympus Image Width");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_HEIGHT, "Olympus Image Height");
+        _tagNameMap.put(TAG_SCENE_DETECT, "Scene Detect");
+        _tagNameMap.put(TAG_SCENE_AREA, "Scene Area");
+        _tagNameMap.put(TAG_SCENE_DETECT_DATA, "Scene Detect Data");
         _tagNameMap.put(TAG_COMPRESSION_RATIO, "Compression Ratio");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_VALID, "Preview Image Valid");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_START, "Preview Image Start");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_LENGTH, "Preview Image Length");
+        _tagNameMap.put(TAG_AF_RESULT, "AF Result");
+        _tagNameMap.put(TAG_CCD_SCAN_MODE, "CCD Scan Mode");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_INFINITY_LENS_STEP, "Infinity Lens Step");
+        _tagNameMap.put(TAG_NEAR_LENS_STEP, "Near Lens Step");
+        _tagNameMap.put(TAG_LIGHT_VALUE_CENTER, "Light Value Center");
+        _tagNameMap.put(TAG_LIGHT_VALUE_PERIPHERY, "Light Value Periphery");
+        _tagNameMap.put(TAG_FIELD_COUNT, "Field Count");
+        _tagNameMap.put(TAG_EQUIPMENT, "Equipment");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings");
+        _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(TAG_MAIN_INFO, "Main Info");
 
         _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
         _tagNameMap.put(CameraSettings.TAG_FLASH_MODE, "Flash Mode");
@@ -388,4 +504,334 @@ public class OlympusMakernoteDirectory extends Directory
     {
         return _tagNameMap;
     }
+
+    // <summary>
+    // These values are currently decoded only for Olympus models.  Models with
+    // Olympus-style maker notes from other brands such as Acer, BenQ, Hitachi, HP,
+    // Premier, Konica-Minolta, Maginon, Ricoh, Rollei, SeaLife, Sony, Supra,
+    // Vivitar are not listed.
+    // </summary>
+    // <remarks>
+    // Converted from Exiftool version 10.33 created by Phil Harvey
+    // http://www.sno.phy.queensu.ca/~phil/exiftool/
+    // lib\Image\ExifTool\Olympus.pm
+    // </remarks>
+    public static final HashMap<String, String> OlympusCameraTypes = new HashMap<String, String>();
+
+    static {
+        OlympusCameraTypes.put("D4028", "X-2,C-50Z");
+        OlympusCameraTypes.put("D4029", "E-20,E-20N,E-20P");
+        OlympusCameraTypes.put("D4034", "C720UZ");
+        OlympusCameraTypes.put("D4040", "E-1");
+        OlympusCameraTypes.put("D4041", "E-300");
+        OlympusCameraTypes.put("D4083", "C2Z,D520Z,C220Z");
+        OlympusCameraTypes.put("D4106", "u20D,S400D,u400D");
+        OlympusCameraTypes.put("D4120", "X-1");
+        OlympusCameraTypes.put("D4122", "u10D,S300D,u300D");
+        OlympusCameraTypes.put("D4125", "AZ-1");
+        OlympusCameraTypes.put("D4141", "C150,D390");
+        OlympusCameraTypes.put("D4193", "C-5000Z");
+        OlympusCameraTypes.put("D4194", "X-3,C-60Z");
+        OlympusCameraTypes.put("D4199", "u30D,S410D,u410D");
+        OlympusCameraTypes.put("D4205", "X450,D535Z,C370Z");
+        OlympusCameraTypes.put("D4210", "C160,D395");
+        OlympusCameraTypes.put("D4211", "C725UZ");
+        OlympusCameraTypes.put("D4213", "FerrariMODEL2003");
+        OlympusCameraTypes.put("D4216", "u15D");
+        OlympusCameraTypes.put("D4217", "u25D");
+        OlympusCameraTypes.put("D4220", "u-miniD,Stylus V");
+        OlympusCameraTypes.put("D4221", "u40D,S500,uD500");
+        OlympusCameraTypes.put("D4231", "FerrariMODEL2004");
+        OlympusCameraTypes.put("D4240", "X500,D590Z,C470Z");
+        OlympusCameraTypes.put("D4244", "uD800,S800");
+        OlympusCameraTypes.put("D4256", "u720SW,S720SW");
+        OlympusCameraTypes.put("D4261", "X600,D630,FE5500");
+        OlympusCameraTypes.put("D4262", "uD600,S600");
+        OlympusCameraTypes.put("D4301", "u810/S810"); // (yes, "/".  Olympus is not consistent in the notation)
+        OlympusCameraTypes.put("D4302", "u710,S710");
+        OlympusCameraTypes.put("D4303", "u700,S700");
+        OlympusCameraTypes.put("D4304", "FE100,X710");
+        OlympusCameraTypes.put("D4305", "FE110,X705");
+        OlympusCameraTypes.put("D4310", "FE-130,X-720");
+        OlympusCameraTypes.put("D4311", "FE-140,X-725");
+        OlympusCameraTypes.put("D4312", "FE150,X730");
+        OlympusCameraTypes.put("D4313", "FE160,X735");
+        OlympusCameraTypes.put("D4314", "u740,S740");
+        OlympusCameraTypes.put("D4315", "u750,S750");
+        OlympusCameraTypes.put("D4316", "u730/S730");
+        OlympusCameraTypes.put("D4317", "FE115,X715");
+        OlympusCameraTypes.put("D4321", "SP550UZ");
+        OlympusCameraTypes.put("D4322", "SP510UZ");
+        OlympusCameraTypes.put("D4324", "FE170,X760");
+        OlympusCameraTypes.put("D4326", "FE200");
+        OlympusCameraTypes.put("D4327", "FE190/X750"); // (also SX876)
+        OlympusCameraTypes.put("D4328", "u760,S760");
+        OlympusCameraTypes.put("D4330", "FE180/X745"); // (also SX875)
+        OlympusCameraTypes.put("D4331", "u1000/S1000");
+        OlympusCameraTypes.put("D4332", "u770SW,S770SW");
+        OlympusCameraTypes.put("D4333", "FE240/X795");
+        OlympusCameraTypes.put("D4334", "FE210,X775");
+        OlympusCameraTypes.put("D4336", "FE230/X790");
+        OlympusCameraTypes.put("D4337", "FE220,X785");
+        OlympusCameraTypes.put("D4338", "u725SW,S725SW");
+        OlympusCameraTypes.put("D4339", "FE250/X800");
+        OlympusCameraTypes.put("D4341", "u780,S780");
+        OlympusCameraTypes.put("D4343", "u790SW,S790SW");
+        OlympusCameraTypes.put("D4344", "u1020,S1020");
+        OlympusCameraTypes.put("D4346", "FE15,X10");
+        OlympusCameraTypes.put("D4348", "FE280,X820,C520");
+        OlympusCameraTypes.put("D4349", "FE300,X830");
+        OlympusCameraTypes.put("D4350", "u820,S820");
+        OlympusCameraTypes.put("D4351", "u1200,S1200");
+        OlympusCameraTypes.put("D4352", "FE270,X815,C510");
+        OlympusCameraTypes.put("D4353", "u795SW,S795SW");
+        OlympusCameraTypes.put("D4354", "u1030SW,S1030SW");
+        OlympusCameraTypes.put("D4355", "SP560UZ");
+        OlympusCameraTypes.put("D4356", "u1010,S1010");
+        OlympusCameraTypes.put("D4357", "u830,S830");
+        OlympusCameraTypes.put("D4359", "u840,S840");
+        OlympusCameraTypes.put("D4360", "FE350WIDE,X865");
+        OlympusCameraTypes.put("D4361", "u850SW,S850SW");
+        OlympusCameraTypes.put("D4362", "FE340,X855,C560");
+        OlympusCameraTypes.put("D4363", "FE320,X835,C540");
+        OlympusCameraTypes.put("D4364", "SP570UZ");
+        OlympusCameraTypes.put("D4366", "FE330,X845,C550");
+        OlympusCameraTypes.put("D4368", "FE310,X840,C530");
+        OlympusCameraTypes.put("D4370", "u1050SW,S1050SW");
+        OlympusCameraTypes.put("D4371", "u1060,S1060");
+        OlympusCameraTypes.put("D4372", "FE370,X880,C575");
+        OlympusCameraTypes.put("D4374", "SP565UZ");
+        OlympusCameraTypes.put("D4377", "u1040,S1040");
+        OlympusCameraTypes.put("D4378", "FE360,X875,C570");
+        OlympusCameraTypes.put("D4379", "FE20,X15,C25");
+        OlympusCameraTypes.put("D4380", "uT6000,ST6000");
+        OlympusCameraTypes.put("D4381", "uT8000,ST8000");
+        OlympusCameraTypes.put("D4382", "u9000,S9000");
+        OlympusCameraTypes.put("D4384", "SP590UZ");
+        OlympusCameraTypes.put("D4385", "FE3010,X895");
+        OlympusCameraTypes.put("D4386", "FE3000,X890");
+        OlympusCameraTypes.put("D4387", "FE35,X30");
+        OlympusCameraTypes.put("D4388", "u550WP,S550WP");
+        OlympusCameraTypes.put("D4390", "FE5000,X905");
+        OlympusCameraTypes.put("D4391", "u5000");
+        OlympusCameraTypes.put("D4392", "u7000,S7000");
+        OlympusCameraTypes.put("D4396", "FE5010,X915");
+        OlympusCameraTypes.put("D4397", "FE25,X20");
+        OlympusCameraTypes.put("D4398", "FE45,X40");
+        OlympusCameraTypes.put("D4401", "XZ-1");
+        OlympusCameraTypes.put("D4402", "uT6010,ST6010");
+        OlympusCameraTypes.put("D4406", "u7010,S7010 / u7020,S7020");
+        OlympusCameraTypes.put("D4407", "FE4010,X930");
+        OlympusCameraTypes.put("D4408", "X560WP");
+        OlympusCameraTypes.put("D4409", "FE26,X21");
+        OlympusCameraTypes.put("D4410", "FE4000,X920,X925");
+        OlympusCameraTypes.put("D4411", "FE46,X41,X42");
+        OlympusCameraTypes.put("D4412", "FE5020,X935");
+        OlympusCameraTypes.put("D4413", "uTough-3000");
+        OlympusCameraTypes.put("D4414", "StylusTough-6020");
+        OlympusCameraTypes.put("D4415", "StylusTough-8010");
+        OlympusCameraTypes.put("D4417", "u5010,S5010");
+        OlympusCameraTypes.put("D4418", "u7040,S7040");
+        OlympusCameraTypes.put("D4419", "u9010,S9010");
+        OlympusCameraTypes.put("D4423", "FE4040");
+        OlympusCameraTypes.put("D4424", "FE47,X43");
+        OlympusCameraTypes.put("D4426", "FE4030,X950");
+        OlympusCameraTypes.put("D4428", "FE5030,X965,X960");
+        OlympusCameraTypes.put("D4430", "u7030,S7030");
+        OlympusCameraTypes.put("D4432", "SP600UZ");
+        OlympusCameraTypes.put("D4434", "SP800UZ");
+        OlympusCameraTypes.put("D4439", "FE4020,X940");
+        OlympusCameraTypes.put("D4442", "FE5035");
+        OlympusCameraTypes.put("D4448", "FE4050,X970");
+        OlympusCameraTypes.put("D4450", "FE5050,X985");
+        OlympusCameraTypes.put("D4454", "u-7050");
+        OlympusCameraTypes.put("D4464", "T10,X27");
+        OlympusCameraTypes.put("D4470", "FE5040,X980");
+        OlympusCameraTypes.put("D4472", "TG-310");
+        OlympusCameraTypes.put("D4474", "TG-610");
+        OlympusCameraTypes.put("D4476", "TG-810");
+        OlympusCameraTypes.put("D4478", "VG145,VG140,D715");
+        OlympusCameraTypes.put("D4479", "VG130,D710");
+        OlympusCameraTypes.put("D4480", "VG120,D705");
+        OlympusCameraTypes.put("D4482", "VR310,D720");
+        OlympusCameraTypes.put("D4484", "VR320,D725");
+        OlympusCameraTypes.put("D4486", "VR330,D730");
+        OlympusCameraTypes.put("D4488", "VG110,D700");
+        OlympusCameraTypes.put("D4490", "SP-610UZ");
+        OlympusCameraTypes.put("D4492", "SZ-10");
+        OlympusCameraTypes.put("D4494", "SZ-20");
+        OlympusCameraTypes.put("D4496", "SZ-30MR");
+        OlympusCameraTypes.put("D4498", "SP-810UZ");
+        OlympusCameraTypes.put("D4500", "SZ-11");
+        OlympusCameraTypes.put("D4504", "TG-615");
+        OlympusCameraTypes.put("D4508", "TG-620");
+        OlympusCameraTypes.put("D4510", "TG-820");
+        OlympusCameraTypes.put("D4512", "TG-1");
+        OlympusCameraTypes.put("D4516", "SH-21");
+        OlympusCameraTypes.put("D4519", "SZ-14");
+        OlympusCameraTypes.put("D4520", "SZ-31MR");
+        OlympusCameraTypes.put("D4521", "SH-25MR");
+        OlympusCameraTypes.put("D4523", "SP-720UZ");
+        OlympusCameraTypes.put("D4529", "VG170");
+        OlympusCameraTypes.put("D4531", "XZ-2");
+        OlympusCameraTypes.put("D4535", "SP-620UZ");
+        OlympusCameraTypes.put("D4536", "TG-320");
+        OlympusCameraTypes.put("D4537", "VR340,D750");
+        OlympusCameraTypes.put("D4538", "VG160,X990,D745");
+        OlympusCameraTypes.put("D4541", "SZ-12");
+        OlympusCameraTypes.put("D4545", "VH410");
+        OlympusCameraTypes.put("D4546", "XZ-10"); //IB
+        OlympusCameraTypes.put("D4547", "TG-2");
+        OlympusCameraTypes.put("D4548", "TG-830");
+        OlympusCameraTypes.put("D4549", "TG-630");
+        OlympusCameraTypes.put("D4550", "SH-50");
+        OlympusCameraTypes.put("D4553", "SZ-16,DZ-105");
+        OlympusCameraTypes.put("D4562", "SP-820UZ");
+        OlympusCameraTypes.put("D4566", "SZ-15");
+        OlympusCameraTypes.put("D4572", "STYLUS1");
+        OlympusCameraTypes.put("D4574", "TG-3");
+        OlympusCameraTypes.put("D4575", "TG-850");
+        OlympusCameraTypes.put("D4579", "SP-100EE");
+        OlympusCameraTypes.put("D4580", "SH-60");
+        OlympusCameraTypes.put("D4581", "SH-1");
+        OlympusCameraTypes.put("D4582", "TG-835");
+        OlympusCameraTypes.put("D4585", "SH-2 / SH-3");
+        OlympusCameraTypes.put("D4586", "TG-4");
+        OlympusCameraTypes.put("D4587", "TG-860");
+        OlympusCameraTypes.put("D4591", "TG-870");
+        OlympusCameraTypes.put("D4809", "C2500L");
+        OlympusCameraTypes.put("D4842", "E-10");
+        OlympusCameraTypes.put("D4856", "C-1");
+        OlympusCameraTypes.put("D4857", "C-1Z,D-150Z");
+        OlympusCameraTypes.put("DCHC", "D500L");
+        OlympusCameraTypes.put("DCHT", "D600L / D620L");
+        OlympusCameraTypes.put("K0055", "AIR-A01");
+        OlympusCameraTypes.put("S0003", "E-330");
+        OlympusCameraTypes.put("S0004", "E-500");
+        OlympusCameraTypes.put("S0009", "E-400");
+        OlympusCameraTypes.put("S0010", "E-510");
+        OlympusCameraTypes.put("S0011", "E-3");
+        OlympusCameraTypes.put("S0013", "E-410");
+        OlympusCameraTypes.put("S0016", "E-420");
+        OlympusCameraTypes.put("S0017", "E-30");
+        OlympusCameraTypes.put("S0018", "E-520");
+        OlympusCameraTypes.put("S0019", "E-P1");
+        OlympusCameraTypes.put("S0023", "E-620");
+        OlympusCameraTypes.put("S0026", "E-P2");
+        OlympusCameraTypes.put("S0027", "E-PL1");
+        OlympusCameraTypes.put("S0029", "E-450");
+        OlympusCameraTypes.put("S0030", "E-600");
+        OlympusCameraTypes.put("S0032", "E-P3");
+        OlympusCameraTypes.put("S0033", "E-5");
+        OlympusCameraTypes.put("S0034", "E-PL2");
+        OlympusCameraTypes.put("S0036", "E-M5");
+        OlympusCameraTypes.put("S0038", "E-PL3");
+        OlympusCameraTypes.put("S0039", "E-PM1");
+        OlympusCameraTypes.put("S0040", "E-PL1s");
+        OlympusCameraTypes.put("S0042", "E-PL5");
+        OlympusCameraTypes.put("S0043", "E-PM2");
+        OlympusCameraTypes.put("S0044", "E-P5");
+        OlympusCameraTypes.put("S0045", "E-PL6");
+        OlympusCameraTypes.put("S0046", "E-PL7"); //IB
+        OlympusCameraTypes.put("S0047", "E-M1");
+        OlympusCameraTypes.put("S0051", "E-M10");
+        OlympusCameraTypes.put("S0052", "E-M5MarkII"); //IB
+        OlympusCameraTypes.put("S0059", "E-M10MarkII");
+        OlympusCameraTypes.put("S0061", "PEN-F"); //forum7005
+        OlympusCameraTypes.put("S0065", "E-PL8");
+        OlympusCameraTypes.put("S0067", "E-M1MarkII");
+        OlympusCameraTypes.put("SR45", "D220");
+        OlympusCameraTypes.put("SR55", "D320L");
+        OlympusCameraTypes.put("SR83", "D340L");
+        OlympusCameraTypes.put("SR85", "C830L,D340R");
+        OlympusCameraTypes.put("SR852", "C860L,D360L");
+        OlympusCameraTypes.put("SR872", "C900Z,D400Z");
+        OlympusCameraTypes.put("SR874", "C960Z,D460Z");
+        OlympusCameraTypes.put("SR951", "C2000Z");
+        OlympusCameraTypes.put("SR952", "C21");
+        OlympusCameraTypes.put("SR953", "C21T.commu");
+        OlympusCameraTypes.put("SR954", "C2020Z");
+        OlympusCameraTypes.put("SR955", "C990Z,D490Z");
+        OlympusCameraTypes.put("SR956", "C211Z");
+        OlympusCameraTypes.put("SR959", "C990ZS,D490Z");
+        OlympusCameraTypes.put("SR95A", "C2100UZ");
+        OlympusCameraTypes.put("SR971", "C100,D370");
+        OlympusCameraTypes.put("SR973", "C2,D230");
+        OlympusCameraTypes.put("SX151", "E100RS");
+        OlympusCameraTypes.put("SX351", "C3000Z / C3030Z");
+        OlympusCameraTypes.put("SX354", "C3040Z");
+        OlympusCameraTypes.put("SX355", "C2040Z");
+        OlympusCameraTypes.put("SX357", "C700UZ");
+        OlympusCameraTypes.put("SX358", "C200Z,D510Z");
+        OlympusCameraTypes.put("SX374", "C3100Z,C3020Z");
+        OlympusCameraTypes.put("SX552", "C4040Z");
+        OlympusCameraTypes.put("SX553", "C40Z,D40Z");
+        OlympusCameraTypes.put("SX556", "C730UZ");
+        OlympusCameraTypes.put("SX558", "C5050Z");
+        OlympusCameraTypes.put("SX571", "C120,D380");
+        OlympusCameraTypes.put("SX574", "C300Z,D550Z");
+        OlympusCameraTypes.put("SX575", "C4100Z,C4000Z");
+        OlympusCameraTypes.put("SX751", "X200,D560Z,C350Z");
+        OlympusCameraTypes.put("SX752", "X300,D565Z,C450Z");
+        OlympusCameraTypes.put("SX753", "C750UZ");
+        OlympusCameraTypes.put("SX754", "C740UZ");
+        OlympusCameraTypes.put("SX755", "C755UZ");
+        OlympusCameraTypes.put("SX756", "C5060WZ");
+        OlympusCameraTypes.put("SX757", "C8080WZ");
+        OlympusCameraTypes.put("SX758", "X350,D575Z,C360Z");
+        OlympusCameraTypes.put("SX759", "X400,D580Z,C460Z");
+        OlympusCameraTypes.put("SX75A", "AZ-2ZOOM");
+        OlympusCameraTypes.put("SX75B", "D595Z,C500Z");
+        OlympusCameraTypes.put("SX75C", "X550,D545Z,C480Z");
+        OlympusCameraTypes.put("SX75D", "IR-300");
+        OlympusCameraTypes.put("SX75F", "C55Z,C5500Z");
+        OlympusCameraTypes.put("SX75G", "C170,D425");
+        OlympusCameraTypes.put("SX75J", "C180,D435");
+        OlympusCameraTypes.put("SX771", "C760UZ");
+        OlympusCameraTypes.put("SX772", "C770UZ");
+        OlympusCameraTypes.put("SX773", "C745UZ");
+        OlympusCameraTypes.put("SX774", "X250,D560Z,C350Z");
+        OlympusCameraTypes.put("SX775", "X100,D540Z,C310Z");
+        OlympusCameraTypes.put("SX776", "C460ZdelSol");
+        OlympusCameraTypes.put("SX777", "C765UZ");
+        OlympusCameraTypes.put("SX77A", "D555Z,C315Z");
+        OlympusCameraTypes.put("SX851", "C7070WZ");
+        OlympusCameraTypes.put("SX852", "C70Z,C7000Z");
+        OlympusCameraTypes.put("SX853", "SP500UZ");
+        OlympusCameraTypes.put("SX854", "SP310");
+        OlympusCameraTypes.put("SX855", "SP350");
+        OlympusCameraTypes.put("SX873", "SP320");
+        OlympusCameraTypes.put("SX875", "FE180/X745"); // (also D4330)
+        OlympusCameraTypes.put("SX876", "FE190/X750"); // (also D4327)
+
+        //   other brands
+        //    4MP9Q3", "Camera 4MP-9Q3'
+        //    4MP9T2", "BenQ DC C420 / Camera 4MP-9T2'
+        //    5MP9Q3", "Camera 5MP-9Q3" },
+        //    5MP9X9", "Camera 5MP-9X9" },
+        //   '5MP-9T'=> 'Camera 5MP-9T3" },
+        //   '5MP-9Y'=> 'Camera 5MP-9Y2" },
+        //   '6MP-9U'=> 'Camera 6MP-9U9" },
+        //    7MP9Q3", "Camera 7MP-9Q3" },
+        //   '8MP-9U'=> 'Camera 8MP-9U4" },
+        //    CE5330", "Acer CE-5330" },
+        //   'CP-853'=> 'Acer CP-8531" },
+        //    CS5531", "Acer CS5531" },
+        //    DC500 ", "SeaLife DC500" },
+        //    DC7370", "Camera 7MP-9GA" },
+        //    DC7371", "Camera 7MP-9GM" },
+        //    DC7371", "Hitachi HDC-751E" },
+        //    DC7375", "Hitachi HDC-763E / Rollei RCP-7330X / Ricoh Caplio RR770 / Vivitar ViviCam 7330" },
+        //   'DC E63'=> 'BenQ DC E63+" },
+        //   'DC P86'=> 'BenQ DC P860" },
+        //    DS5340", "Maginon Performic S5 / Premier 5MP-9M7" },
+        //    DS5341", "BenQ E53+ / Supra TCM X50 / Maginon X50 / Premier 5MP-9P8" },
+        //    DS5346", "Premier 5MP-9Q2" },
+        //    E500  ", "Konica Minolta DiMAGE E500" },
+        //    MAGINO", "Maginon X60" },
+        //    Mz60  ", "HP Photosmart Mz60" },
+        //    Q3DIGI", "Camera 5MP-9Q3" },
+        //    SLIMLI", "Supra Slimline X6" },
+        //    V8300s", "Vivitar V8300s" },
+    }
 }
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java
new file mode 100644
index 0000000..90743fa
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.util.HashMap;
+
+import static com.drew.metadata.exif.makernotes.OlympusRawDevelopment2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusRawDevelopment2MakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopment2MakernoteDescriptor extends TagDescriptor<OlympusRawDevelopment2MakernoteDirectory>
+{
+    public OlympusRawDevelopment2MakernoteDescriptor(@NotNull OlympusRawDevelopment2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagRawDevVersion:
+                return getRawDevVersionDescription();
+            case TagRawDevExposureBiasValue:
+                return getRawDevExposureBiasValueDescription();
+            case TagRawDevColorSpace:
+                return getRawDevColorSpaceDescription();
+            case TagRawDevNoiseReduction:
+                return getRawDevNoiseReductionDescription();
+            case TagRawDevEngine:
+                return getRawDevEngineDescription();
+            case TagRawDevPictureMode:
+                return getRawDevPictureModeDescription();
+            case TagRawDevPmBwFilter:
+                return getRawDevPmBwFilterDescription();
+            case TagRawDevPmPictureTone:
+                return getRawDevPmPictureToneDescription();
+            case TagRawDevArtFilter:
+                return getRawDevArtFilterDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getRawDevVersionDescription()
+    {
+        return getVersionBytesDescription(TagRawDevVersion, 4);
+    }
+
+    @Nullable
+    public String getRawDevExposureBiasValueDescription()
+    {
+        return getIndexedDescription(TagRawDevExposureBiasValue,
+                1, "Color Temperature", "Gray Point");
+    }
+
+    @Nullable
+    public String getRawDevColorSpaceDescription()
+    {
+        return getIndexedDescription(TagRawDevColorSpace,
+            "sRGB", "Adobe RGB", "Pro Photo RGB");
+    }
+
+    @Nullable
+    public String getRawDevNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevNoiseReduction);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if ((v        & 1) != 0) sb.append("Noise Reduction, ");
+        if (((v >> 1) & 1) != 0) sb.append("Noise Filter, ");
+        if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getRawDevEngineDescription()
+    {
+        return getIndexedDescription(TagRawDevEngine,
+            "High Speed", "High Function", "Advanced High Speed", "Advanced High Function");
+    }
+
+    @Nullable
+    public String getRawDevPictureModeDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevPictureMode);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 1:
+                return "Vivid";
+            case 2:
+                return "Natural";
+            case 3:
+                return "Muted";
+            case 256:
+                return "Monotone";
+            case 512:
+                return "Sepia";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getRawDevPmBwFilterDescription()
+    {
+        return getIndexedDescription(TagRawDevPmBwFilter,
+            "Neutral", "Yellow", "Orange", "Red", "Green");
+    }
+
+    @Nullable
+    public String getRawDevPmPictureToneDescription()
+    {
+        return getIndexedDescription(TagRawDevPmPictureTone,
+            "Neutral", "Sepia", "Blue", "Purple", "Green");
+    }
+
+    @Nullable
+    public String getRawDevArtFilterDescription()
+    {
+        return getFilterDescription(TagRawDevArtFilter);
+    }
+
+    @Nullable
+    public String getFilterDescription(int tag)
+    {
+        int[] values = _directory.getIntArray(tag);
+        if (values == null || values.length == 0)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            if (i == 0)
+                sb.append(_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]");
+            else
+                sb.append(values[i]).append("; ");
+            sb.append("; ");
+        }
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    // RawDevArtFilter values
+    private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>();
+
+    static {
+        _filters.put(0, "Off");
+        _filters.put(1, "Soft Focus");
+        _filters.put(2, "Pop Art");
+        _filters.put(3, "Pale & Light Color");
+        _filters.put(4, "Light Tone");
+        _filters.put(5, "Pin Hole");
+        _filters.put(6, "Grainy Film");
+        _filters.put(9, "Diorama");
+        _filters.put(10, "Cross Process");
+        _filters.put(12, "Fish Eye");
+        _filters.put(13, "Drawing");
+        _filters.put(14, "Gentle Sepia");
+        _filters.put(15, "Pale & Light Color II");
+        _filters.put(16, "Pop Art II");
+        _filters.put(17, "Pin Hole II");
+        _filters.put(18, "Pin Hole III");
+        _filters.put(19, "Grainy Film II");
+        _filters.put(20, "Dramatic Tone");
+        _filters.put(21, "Punk");
+        _filters.put(22, "Soft Focus 2");
+        _filters.put(23, "Sparkle");
+        _filters.put(24, "Watercolor");
+        _filters.put(25, "Key Line");
+        _filters.put(26, "Key Line II");
+        _filters.put(27, "Miniature");
+        _filters.put(28, "Reflection");
+        _filters.put(29, "Fragmented");
+        _filters.put(31, "Cross Process II");
+        _filters.put(32, "Dramatic Tone II");
+        _filters.put(33, "Watercolor I");
+        _filters.put(34, "Watercolor II");
+        _filters.put(35, "Diorama II");
+        _filters.put(36, "Vintage");
+        _filters.put(37, "Vintage II");
+        _filters.put(38, "Vintage III");
+        _filters.put(39, "Partial Color");
+        _filters.put(40, "Partial Color II");
+        _filters.put(41, "Partial Color III");
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java
new file mode 100644
index 0000000..e25b8b1
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * The Olympus raw development 2 makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopment2MakernoteDirectory extends Directory
+{    
+    public static final int TagRawDevVersion = 0x0000;
+    public static final int TagRawDevExposureBiasValue = 0x0100;
+    public static final int TagRawDevWhiteBalance = 0x0101;
+    public static final int TagRawDevWhiteBalanceValue = 0x0102;
+    public static final int TagRawDevWbFineAdjustment = 0x0103;
+    public static final int TagRawDevGrayPoint = 0x0104;
+    public static final int TagRawDevContrastValue = 0x0105;
+    public static final int TagRawDevSharpnessValue = 0x0106;
+    public static final int TagRawDevSaturationEmphasis = 0x0107;
+    public static final int TagRawDevMemoryColorEmphasis = 0x0108;
+    public static final int TagRawDevColorSpace = 0x0109;
+    public static final int TagRawDevNoiseReduction = 0x010a;
+    public static final int TagRawDevEngine = 0x010b;
+    public static final int TagRawDevPictureMode = 0x010c;
+    public static final int TagRawDevPmSaturation = 0x010d;
+    public static final int TagRawDevPmContrast = 0x010e;
+    public static final int TagRawDevPmSharpness = 0x010f;
+    public static final int TagRawDevPmBwFilter = 0x0110;
+    public static final int TagRawDevPmPictureTone = 0x0111;
+    public static final int TagRawDevGradation = 0x0112;
+    public static final int TagRawDevSaturation3 = 0x0113;
+    public static final int TagRawDevAutoGradation = 0x0119;
+    public static final int TagRawDevPmNoiseFilter = 0x0120;
+    public static final int TagRawDevArtFilter = 0x0121;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {        
+        _tagNameMap.put(TagRawDevVersion, "Raw Dev Version");
+        _tagNameMap.put(TagRawDevExposureBiasValue, "Raw Dev Exposure Bias Value");
+        _tagNameMap.put(TagRawDevWhiteBalance, "Raw Dev White Balance");
+        _tagNameMap.put(TagRawDevWhiteBalanceValue, "Raw Dev White Balance Value");
+        _tagNameMap.put(TagRawDevWbFineAdjustment, "Raw Dev WB Fine Adjustment");
+        _tagNameMap.put(TagRawDevGrayPoint, "Raw Dev Gray Point");
+        _tagNameMap.put(TagRawDevContrastValue, "Raw Dev Contrast Value");
+        _tagNameMap.put(TagRawDevSharpnessValue, "Raw Dev Sharpness Value");
+        _tagNameMap.put(TagRawDevSaturationEmphasis, "Raw Dev Saturation Emphasis");
+        _tagNameMap.put(TagRawDevMemoryColorEmphasis, "Raw Dev Memory Color Emphasis");
+        _tagNameMap.put(TagRawDevColorSpace, "Raw Dev Color Space");
+        _tagNameMap.put(TagRawDevNoiseReduction, "Raw Dev Noise Reduction");
+        _tagNameMap.put(TagRawDevEngine, "Raw Dev Engine");
+        _tagNameMap.put(TagRawDevPictureMode, "Raw Dev Picture Mode");
+        _tagNameMap.put(TagRawDevPmSaturation, "Raw Dev PM Saturation");
+        _tagNameMap.put(TagRawDevPmContrast, "Raw Dev PM Contrast");
+        _tagNameMap.put(TagRawDevPmSharpness, "Raw Dev PM Sharpness");
+        _tagNameMap.put(TagRawDevPmBwFilter, "Raw Dev PM BW Filter");
+        _tagNameMap.put(TagRawDevPmPictureTone, "Raw Dev PM Picture Tone");
+        _tagNameMap.put(TagRawDevGradation, "Raw Dev Gradation");
+        _tagNameMap.put(TagRawDevSaturation3, "Raw Dev Saturation 3");
+        _tagNameMap.put(TagRawDevAutoGradation, "Raw Dev Auto Gradation");
+        _tagNameMap.put(TagRawDevPmNoiseFilter, "Raw Dev PM Noise Filter");
+        _tagNameMap.put(TagRawDevArtFilter, "Raw Dev Art Filter");
+    }
+
+    public OlympusRawDevelopment2MakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusRawDevelopment2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Raw Development 2";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java
new file mode 100644
index 0000000..d7c88c4
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusRawDevelopmentMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusRawDevelopmentMakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.10 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopmentMakernoteDescriptor extends TagDescriptor<OlympusRawDevelopmentMakernoteDirectory>
+{
+    public OlympusRawDevelopmentMakernoteDescriptor(@NotNull OlympusRawDevelopmentMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagRawDevVersion:
+                return getRawDevVersionDescription();
+            case TagRawDevColorSpace:
+                return getRawDevColorSpaceDescription();
+            case TagRawDevEngine:
+                return getRawDevEngineDescription();
+            case TagRawDevNoiseReduction:
+                return getRawDevNoiseReductionDescription();
+            case TagRawDevEditStatus:
+                return getRawDevEditStatusDescription();
+            case TagRawDevSettings:
+                return getRawDevSettingsDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getRawDevVersionDescription()
+    {
+        return getVersionBytesDescription(TagRawDevVersion, 4);
+    }
+
+    @Nullable
+    public String getRawDevColorSpaceDescription()
+    {
+        return getIndexedDescription(TagRawDevColorSpace,
+            "sRGB", "Adobe RGB", "Pro Photo RGB");
+    }
+
+    @Nullable
+    public String getRawDevEngineDescription()
+    {
+        return getIndexedDescription(TagRawDevEngine,
+            "High Speed", "High Function", "Advanced High Speed", "Advanced High Function");
+    }
+
+    @Nullable
+    public String getRawDevNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevNoiseReduction);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if ((v        & 1) != 0) sb.append("Noise Reduction, ");
+        if (((v >> 1) & 1) != 0) sb.append("Noise Filter, ");
+        if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+    @Nullable
+    public String getRawDevEditStatusDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevEditStatus);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "Original";
+            case 1:
+                return "Edited (Landscape)";
+            case 6:
+            case 8:
+                return "Edited (Portrait)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getRawDevSettingsDescription()
+    {
+        Integer value = _directory.getInteger(TagRawDevSettings);
+        if (value == null)
+            return null;
+
+        if (value == 0)
+            return "(none)";
+
+        StringBuilder sb = new StringBuilder();
+        int v = value;
+
+        if ((v        & 1) != 0) sb.append("WB Color Temp, ");
+        if (((v >> 1) & 1) != 0) sb.append("WB Gray Point, ");
+        if (((v >> 2) & 1) != 0) sb.append("Saturation, ");
+        if (((v >> 3) & 1) != 0) sb.append("Contrast, ");
+        if (((v >> 4) & 1) != 0) sb.append("Sharpness, ");
+        if (((v >> 5) & 1) != 0) sb.append("Color Space, ");
+        if (((v >> 6) & 1) != 0) sb.append("High Function, ");
+        if (((v >> 7) & 1) != 0) sb.append("Noise Reduction, ");
+
+        return sb.substring(0, sb.length() - 2);
+    }
+
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java
new file mode 100644
index 0000000..13ec7e5
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * The Olympus raw development makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawDevelopmentMakernoteDirectory extends Directory
+{
+    public static final int TagRawDevVersion = 0x0000;
+    public static final int TagRawDevExposureBiasValue = 0x0100;
+    public static final int TagRawDevWhiteBalanceValue = 0x0101;
+    public static final int TagRawDevWbFineAdjustment = 0x0102;
+    public static final int TagRawDevGrayPoint = 0x0103;
+    public static final int TagRawDevSaturationEmphasis = 0x0104;
+    public static final int TagRawDevMemoryColorEmphasis = 0x0105;
+    public static final int TagRawDevContrastValue = 0x0106;
+    public static final int TagRawDevSharpnessValue = 0x0107;
+    public static final int TagRawDevColorSpace = 0x0108;
+    public static final int TagRawDevEngine = 0x0109;
+    public static final int TagRawDevNoiseReduction = 0x010a;
+    public static final int TagRawDevEditStatus = 0x010b;
+    public static final int TagRawDevSettings = 0x010c;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagRawDevVersion, "Raw Dev Version");
+        _tagNameMap.put(TagRawDevExposureBiasValue, "Raw Dev Exposure Bias Value");
+        _tagNameMap.put(TagRawDevWhiteBalanceValue, "Raw Dev White Balance Value");
+        _tagNameMap.put(TagRawDevWbFineAdjustment, "Raw Dev WB Fine Adjustment");
+        _tagNameMap.put(TagRawDevGrayPoint, "Raw Dev Gray Point");
+        _tagNameMap.put(TagRawDevSaturationEmphasis, "Raw Dev Saturation Emphasis");
+        _tagNameMap.put(TagRawDevMemoryColorEmphasis, "Raw Dev Memory Color Emphasis");
+        _tagNameMap.put(TagRawDevContrastValue, "Raw Dev Contrast Value");
+        _tagNameMap.put(TagRawDevSharpnessValue, "Raw Dev Sharpness Value");
+        _tagNameMap.put(TagRawDevColorSpace, "Raw Dev Color Space");
+        _tagNameMap.put(TagRawDevEngine, "Raw Dev Engine");
+        _tagNameMap.put(TagRawDevNoiseReduction, "Raw Dev Noise Reduction");
+        _tagNameMap.put(TagRawDevEditStatus, "Raw Dev Edit Status");
+        _tagNameMap.put(TagRawDevSettings, "Raw Dev Settings");
+    }
+
+    public OlympusRawDevelopmentMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusRawDevelopmentMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Raw Development";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java
new file mode 100644
index 0000000..aac65ae
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.OlympusRawInfoMakernoteDirectory.*;
+
+/**
+ * Provides human-readable String representations of tag values stored in a {@link OlympusRawInfoMakernoteDirectory}.
+ * <p>
+ * Some Description functions converted from Exiftool version 10.33 created by Phil Harvey
+ * http://www.sno.phy.queensu.ca/~phil/exiftool/
+ * lib\Image\ExifTool\Olympus.pm
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawInfoMakernoteDescriptor extends TagDescriptor<OlympusRawInfoMakernoteDirectory>
+{
+    public OlympusRawInfoMakernoteDescriptor(@NotNull OlympusRawInfoMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagRawInfoVersion:
+                return getVersionBytesDescription(TagRawInfoVersion, 4);
+            case TagColorMatrix2:
+                return getColorMatrix2Description();
+            case TagYCbCrCoefficients:
+                return getYCbCrCoefficientsDescription();
+            case TagLightSource:
+                return getOlympusLightSourceDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getColorMatrix2Description()
+    {
+        int[] values = _directory.getIntArray(TagColorMatrix2);
+        if (values == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < values.length; i++) {
+            sb.append((short)values[i]);
+            if (i < values.length - 1)
+                sb.append(" ");
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    @Nullable
+    public String getYCbCrCoefficientsDescription()
+    {
+        int[] values = _directory.getIntArray(TagYCbCrCoefficients);
+        if (values == null)
+            return null;
+
+        Rational[] ret = new Rational[values.length / 2];
+        for(int i = 0; i < values.length / 2; i++)
+        {
+            ret[i] = new Rational((short)values[2*i], (short)values[2*i + 1]);
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < ret.length; i++) {
+            sb.append(ret[i].doubleValue());
+            if (i < ret.length - 1)
+                sb.append(" ");
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+    
+    @Nullable
+    public String getOlympusLightSourceDescription()
+    {
+        Integer value = _directory.getInteger(TagLightSource);
+        if (value == null)
+            return null;
+
+        switch (value.shortValue())
+        {
+            case 0:
+                return "Unknown";
+            case 16:
+                return "Shade";
+            case 17:
+                return "Cloudy";
+            case 18:
+                return "Fine Weather";
+            case 20:
+                return "Tungsten (Incandescent)";
+            case 22:
+                return "Evening Sunlight";
+            case 33:
+                return "Daylight Fluorescent";
+            case 34:
+                return "Day White Fluorescent";
+            case 35:
+                return "Cool White Fluorescent";
+            case 36:
+                return "White Fluorescent";
+            case 256:
+                return "One Touch White Balance";
+            case 512:
+                return "Custom 1-4";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java
new file mode 100644
index 0000000..629cf60
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * These tags are found only in ORF images of some models (eg. C8080WZ)
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class OlympusRawInfoMakernoteDirectory extends Directory
+{
+    public static final int TagRawInfoVersion = 0x0000;
+    public static final int TagWbRbLevelsUsed = 0x0100;
+    public static final int TagWbRbLevelsAuto = 0x0110;
+    public static final int TagWbRbLevelsShade = 0x0120;
+    public static final int TagWbRbLevelsCloudy = 0x0121;
+    public static final int TagWbRbLevelsFineWeather = 0x0122;
+    public static final int TagWbRbLevelsTungsten = 0x0123;
+    public static final int TagWbRbLevelsEveningSunlight = 0x0124;
+    public static final int TagWbRbLevelsDaylightFluor = 0x0130;
+    public static final int TagWbRbLevelsDayWhiteFluor = 0x0131;
+    public static final int TagWbRbLevelsCoolWhiteFluor = 0x0132;
+    public static final int TagWbRbLevelsWhiteFluorescent = 0x0133;
+
+    public static final int TagColorMatrix2 = 0x0200;
+    public static final int TagCoringFilter = 0x0310;
+    public static final int TagCoringValues = 0x0311;
+    public static final int TagBlackLevel2 = 0x0600;
+    public static final int TagYCbCrCoefficients = 0x0601;
+    public static final int TagValidPixelDepth = 0x0611;
+    public static final int TagCropLeft = 0x0612;
+    public static final int TagCropTop = 0x0613;
+    public static final int TagCropWidth = 0x0614;
+    public static final int TagCropHeight = 0x0615;
+
+    public static final int TagLightSource = 0x1000;
+
+    //the following 5 tags all have 3 values: val, min, max
+    public static final int TagWhiteBalanceComp = 0x1001;
+    public static final int TagSaturationSetting = 0x1010;
+    public static final int TagHueSetting = 0x1011;
+    public static final int TagContrastSetting = 0x1012;
+    public static final int TagSharpnessSetting = 0x1013;
+
+    // settings written by Camedia Master 4.x
+    public static final int TagCmExposureCompensation = 0x2000;
+    public static final int TagCmWhiteBalance = 0x2001;
+    public static final int TagCmWhiteBalanceComp = 0x2002;
+    public static final int TagCmWhiteBalanceGrayPoint = 0x2010;
+    public static final int TagCmSaturation = 0x2020;
+    public static final int TagCmHue = 0x2021;
+    public static final int TagCmContrast = 0x2022;
+    public static final int TagCmSharpness = 0x2023;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TagRawInfoVersion, "Raw Info Version");
+        _tagNameMap.put(TagWbRbLevelsUsed, "WB RB Levels Used");
+        _tagNameMap.put(TagWbRbLevelsAuto, "WB RB Levels Auto");
+        _tagNameMap.put(TagWbRbLevelsShade, "WB RB Levels Shade");
+        _tagNameMap.put(TagWbRbLevelsCloudy, "WB RB Levels Cloudy");
+        _tagNameMap.put(TagWbRbLevelsFineWeather, "WB RB Levels Fine Weather");
+        _tagNameMap.put(TagWbRbLevelsTungsten, "WB RB Levels Tungsten");
+        _tagNameMap.put(TagWbRbLevelsEveningSunlight, "WB RB Levels Evening Sunlight");
+        _tagNameMap.put(TagWbRbLevelsDaylightFluor, "WB RB Levels Daylight Fluor");
+        _tagNameMap.put(TagWbRbLevelsDayWhiteFluor, "WB RB Levels Day White Fluor");
+        _tagNameMap.put(TagWbRbLevelsCoolWhiteFluor, "WB RB Levels Cool White Fluor");
+        _tagNameMap.put(TagWbRbLevelsWhiteFluorescent, "WB RB Levels White Fluorescent");
+        _tagNameMap.put(TagColorMatrix2, "Color Matrix 2");
+        _tagNameMap.put(TagCoringFilter, "Coring Filter");
+        _tagNameMap.put(TagCoringValues, "Coring Values");
+        _tagNameMap.put(TagBlackLevel2, "Black Level 2");
+        _tagNameMap.put(TagYCbCrCoefficients, "YCbCrCoefficients");
+        _tagNameMap.put(TagValidPixelDepth, "Valid Pixel Depth");
+        _tagNameMap.put(TagCropLeft, "Crop Left");
+        _tagNameMap.put(TagCropTop, "Crop Top");
+        _tagNameMap.put(TagCropWidth, "Crop Width");
+        _tagNameMap.put(TagCropHeight, "Crop Height");
+        _tagNameMap.put(TagLightSource, "Light Source");
+
+        _tagNameMap.put(TagWhiteBalanceComp, "White Balance Comp");
+        _tagNameMap.put(TagSaturationSetting, "Saturation Setting");
+        _tagNameMap.put(TagHueSetting, "Hue Setting");
+        _tagNameMap.put(TagContrastSetting, "Contrast Setting");
+        _tagNameMap.put(TagSharpnessSetting, "Sharpness Setting");
+
+        _tagNameMap.put(TagCmExposureCompensation, "CM Exposure Compensation");
+        _tagNameMap.put(TagCmWhiteBalance, "CM White Balance");
+        _tagNameMap.put(TagCmWhiteBalanceComp, "CM White Balance Comp");
+        _tagNameMap.put(TagCmWhiteBalanceGrayPoint, "CM White Balance Gray Point");
+        _tagNameMap.put(TagCmSaturation, "CM Saturation");
+        _tagNameMap.put(TagCmHue, "CM Hue");
+        _tagNameMap.put(TagCmContrast, "CM Contrast");
+        _tagNameMap.put(TagCmSharpness, "CM Sharpness");
+    }
+
+    public OlympusRawInfoMakernoteDirectory()
+    {
+        this.setDescriptor(new OlympusRawInfoMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Olympus Raw Info";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
index 71ca518..d5e4943 100644
--- a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
 package com.drew.metadata.exif.makernotes;
 
 import com.drew.lang.ByteArrayReader;
+import com.drew.lang.Charsets;
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
@@ -28,6 +29,7 @@ import com.drew.metadata.Age;
 import com.drew.metadata.Face;
 import com.drew.metadata.TagDescriptor;
 
+import java.text.DecimalFormat;
 import java.io.IOException;
 
 import static com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory.*;
@@ -44,6 +46,7 @@ import static com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory.*;
  * @author Drew Noakes https://drewnoakes.com
  * @author Philipp Sandhaus
  */
+@SuppressWarnings("WeakerAccess")
 public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory>
 {
     public PanasonicMakernoteDescriptor(@NotNull PanasonicMakernoteDirectory directory)
@@ -108,8 +111,8 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno
                 return getDetectedFacesDescription();
             case TAG_TRANSFORM:
                 return getTransformDescription();
-			case TAG_TRANSFORM_1:
-	            return getTransform1Description();
+            case TAG_TRANSFORM_1:
+                return getTransform1Description();
             case TAG_INTELLIGENT_EXPOSURE:
                 return getIntelligentExposureDescription();
             case TAG_FLASH_WARNING:
@@ -126,20 +129,18 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno
                 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();
+                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:
@@ -147,26 +148,61 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno
             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 getTitleDescription();
+            case TAG_BRACKET_SETTINGS:
+                return getBracketSettingsDescription();
+            case TAG_FLASH_CURTAIN:
+                return getFlashCurtainDescription();
+            case TAG_LONG_EXPOSURE_NOISE_REDUCTION:
+                return getLongExposureNoiseReductionDescription();
+            case TAG_BABY_NAME:
+                return getBabyNameDescription();
+            case TAG_LOCATION:
+                return getLocationDescription();
+
+            case TAG_LENS_FIRMWARE_VERSION:
+                return getLensFirmwareVersionDescription();
+            case TAG_INTELLIGENT_D_RANGE:
+                return getIntelligentDRangeDescription();
+            case TAG_CLEAR_RETOUCH:
+                return getClearRetouchDescription();
+            case TAG_PHOTO_STYLE:
+                return getPhotoStyleDescription();
+            case TAG_SHADING_COMPENSATION:
+                return getShadingCompensationDescription();
+
+            case TAG_ACCELEROMETER_Z:
+                return getAccelerometerZDescription();
+            case TAG_ACCELEROMETER_X:
+                return getAccelerometerXDescription();
+            case TAG_ACCELEROMETER_Y:
+                return getAccelerometerYDescription();
+            case TAG_CAMERA_ORIENTATION:
+                return getCameraOrientationDescription();
+            case TAG_ROLL_ANGLE:
+                return getRollAngleDescription();
+            case TAG_PITCH_ANGLE:
+                return getPitchAngleDescription();
+            case TAG_SWEEP_PANORAMA_DIRECTION:
+                return getSweepPanoramaDirectionDescription();
+            case TAG_TIMER_RECORDING:
+                return getTimerRecordingDescription();
+            case TAG_HDR:
+                return getHDRDescription();
+            case TAG_SHUTTER_TYPE:
+                return getShutterTypeDescription();
+            case TAG_TOUCH_AE:
+                return getTouchAeDescription();
+
+            case TAG_BABY_AGE:
+                return getBabyAgeDescription();
+            case TAG_BABY_AGE_1:
+                return getBabyAge1Description();
+            default:
                 return super.getDescription(tagType);
         }
     }
 
-    @Nullable
-    public String getPrintImageMatchingInfoDescription()
-    {
-        return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO);
-    }
-
     @Nullable
     public String getTextStampDescription()
     {
@@ -277,46 +313,241 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno
             "No", "Yes (Flash required but disabled)");
     }
 
+    @Nullable
+    private static String trim(@Nullable String s)
+    {
+        return s == null ? null : s.trim();
+    }
+
     @Nullable
     public String getCountryDescription()
     {
-        return getAsciiStringFromBytes(TAG_COUNTRY);
+        return trim(getStringFromBytes(TAG_COUNTRY, Charsets.UTF_8));
     }
 
     @Nullable
     public String getStateDescription()
     {
-        return getAsciiStringFromBytes(TAG_STATE);
+        return trim(getStringFromBytes(TAG_STATE, Charsets.UTF_8));
     }
 
     @Nullable
     public String getCityDescription()
     {
-        return getAsciiStringFromBytes(TAG_CITY);
+        return trim(getStringFromBytes(TAG_CITY, Charsets.UTF_8));
     }
 
     @Nullable
     public String getLandmarkDescription()
     {
-        return getAsciiStringFromBytes(TAG_LANDMARK);
+        return trim(getStringFromBytes(TAG_LANDMARK, Charsets.UTF_8));
     }
 
-	@Nullable
+    @Nullable
     public String getTitleDescription()
     {
-        return getAsciiStringFromBytes(TAG_TITLE);
+        return trim(getStringFromBytes(TAG_TITLE, Charsets.UTF_8));
     }
 
-	@Nullable
+    @Nullable
+    public String getBracketSettingsDescription()
+    {
+        return getIndexedDescription(TAG_BRACKET_SETTINGS,
+            "No Bracket", "3 Images, Sequence 0/-/+", "3 Images, Sequence -/0/+", "5 Images, Sequence 0/-/+",
+            "5 Images, Sequence -/0/+", "7 Images, Sequence 0/-/+", "7 Images, Sequence -/0/+");
+    }
+
+    @Nullable
+    public String getFlashCurtainDescription()
+    {
+        return getIndexedDescription(TAG_FLASH_CURTAIN,
+            "n/a", "1st", "2nd");
+    }
+
+    @Nullable
+    public String getLongExposureNoiseReductionDescription()
+    {
+        return getIndexedDescription(TAG_LONG_EXPOSURE_NOISE_REDUCTION, 1,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getLensFirmwareVersionDescription()
+    {
+        // lens version has 4 parts separated by periods
+        byte[] bytes = _directory.getByteArray(TAG_LENS_FIRMWARE_VERSION);
+        if (bytes == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < bytes.length; i++) {
+            sb.append(bytes[i]);
+            if (i < bytes.length - 1)
+                sb.append(".");
+        }
+        return sb.toString();
+        //return string.Join(".", bytes.Select(b => b.ToString()).ToArray());
+    }
+
+    @Nullable
+    public String getIntelligentDRangeDescription()
+    {
+        return getIndexedDescription(TAG_INTELLIGENT_D_RANGE,
+            "Off", "Low", "Standard", "High");
+    }
+
+    @Nullable
+    public String getClearRetouchDescription()
+    {
+        return getIndexedDescription(TAG_CLEAR_RETOUCH,
+                "Off", "On");
+
+    }
+
+    @Nullable
+    public String getPhotoStyleDescription()
+    {
+        return getIndexedDescription(TAG_PHOTO_STYLE,
+            "Auto", "Standard or Custom", "Vivid", "Natural", "Monochrome", "Scenery", "Portrait");
+    }
+
+    @Nullable
+    public String getShadingCompensationDescription()
+    {
+        return getIndexedDescription(TAG_SHADING_COMPENSATION,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getAccelerometerZDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACCELEROMETER_Z);
+        if (value == null)
+            return null;
+
+        // positive is acceleration upwards
+        return String.valueOf(value.shortValue());
+    }
+
+    @Nullable
+    public String getAccelerometerXDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACCELEROMETER_X);
+        if (value == null)
+            return null;
+
+        // positive is acceleration to the left
+        return String.valueOf(value.shortValue());
+    }
+
+    @Nullable
+    public String getAccelerometerYDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ACCELEROMETER_Y);
+        if (value == null)
+            return null;
+
+        // positive is acceleration backwards
+        return String.valueOf(value.shortValue());
+    }
+
+    @Nullable
+    public String getCameraOrientationDescription()
+    {
+        return getIndexedDescription(TAG_CAMERA_ORIENTATION,
+                "Normal", "Rotate CW", "Rotate 180", "Rotate CCW", "Tilt Upwards", "Tile Downwards");
+    }
+
+    @Nullable
+    public String getRollAngleDescription()
+    {
+        Integer value = _directory.getInteger(TAG_ROLL_ANGLE);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        // converted to degrees of clockwise camera rotation
+        return format.format(value.shortValue() / 10.0);
+    }
+
+    @Nullable
+    public String getPitchAngleDescription()
+    {
+        Integer value = _directory.getInteger(TAG_PITCH_ANGLE);
+        if (value == null)
+            return null;
+
+        DecimalFormat format = new DecimalFormat("0.#");
+        // converted to degrees of upward camera tilt
+        return format.format(-value.shortValue() / 10.0);
+    }
+
+    @Nullable
+    public String getSweepPanoramaDirectionDescription()
+    {
+        return getIndexedDescription(TAG_SWEEP_PANORAMA_DIRECTION,
+                "Off", "Left to Right", "Right to Left", "Top to Bottom", "Bottom to Top");
+    }
+
+    @Nullable
+    public String getTimerRecordingDescription()
+    {
+        return getIndexedDescription(TAG_TIMER_RECORDING,
+                "Off", "Time Lapse", "Stop-motion Animation");
+    }
+
+    @Nullable
+    public String getHDRDescription()
+    {
+        Integer value = _directory.getInteger(TAG_HDR);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0:
+                return "Off";
+            case 100:
+                return "1 EV";
+            case 200:
+                return "2 EV";
+            case 300:
+                return "3 EV";
+            case 32868:
+                return "1 EV (Auto)";
+            case 32968:
+                return "2 EV (Auto)";
+            case 33068:
+                return "3 EV (Auto)";
+            default:
+                return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getShutterTypeDescription()
+    {
+        return getIndexedDescription(TAG_SHUTTER_TYPE,
+                "Mechanical", "Electronic", "Hybrid");
+    }
+
+    @Nullable
+    public String getTouchAeDescription()
+    {
+        return getIndexedDescription(TAG_TOUCH_AE,
+                "Off", "On");
+    }
+
+    @Nullable
     public String getBabyNameDescription()
     {
-        return getAsciiStringFromBytes(TAG_BABY_NAME);
+        return trim(getStringFromBytes(TAG_BABY_NAME, Charsets.UTF_8));
     }
 
 	@Nullable
     public String getLocationDescription()
     {
-        return getAsciiStringFromBytes(TAG_LOCATION);
+        return trim(getStringFromBytes(TAG_LOCATION, Charsets.UTF_8));
     }
 
     @Nullable
diff --git a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
index a934cec..b2b6c25 100644
--- a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -34,8 +34,10 @@ import java.util.HashMap;
 /**
  * Describes tags specific to Panasonic and Leica cameras.
  *
- * @author Drew Noakes https://drewnoakes.com, Philipp Sandhaus
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Philipp Sandhaus
  */
+@SuppressWarnings("WeakerAccess")
 public class PanasonicMakernoteDirectory extends Directory
 {
 
@@ -352,16 +354,23 @@ public class PanasonicMakernoteDirectory extends Directory
     public static final int TAG_SHARPNESS = 0x0041;
     public static final int TAG_FILM_MODE = 0x0042;
 
+    public static final int TAG_COLOR_TEMP_KELVIN = 0x0044;
+    public static final int TAG_BRACKET_SETTINGS = 0x0045;
+
     /**
-	 * WB adjust AB. Positive is a shift toward blue.
-	 */
-	public static final int TAG_WB_ADJUST_AB = 0x0046;
+    * WB adjust 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;
+    * WB adjust GM. Positive is a shift toward green.
+    */
+    public static final int TAG_WB_ADJUST_GM = 0x0047;
 
+    public static final int TAG_FLASH_CURTAIN = 0x0048;
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION = 0x0049;
 
+    public static final int TAG_PANASONIC_IMAGE_WIDTH = 0x004b;
+    public static final int TAG_PANASONIC_IMAGE_HEIGHT = 0x004c;
     public static final int TAG_AF_POINT_POSITION = 0x004d;
 
 
@@ -382,6 +391,7 @@ public class PanasonicMakernoteDirectory extends Directory
     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;
+    public static final int TAG_ACCESSORY_SERIAL_NUMBER = 0x0054;
 
     /**
      * (decoded as two 16-bit signed integers)
@@ -401,10 +411,35 @@ public class PanasonicMakernoteDirectory extends Directory
     */
     public static final int TAG_INTELLIGENT_EXPOSURE = 0x005d;
 
-    /**
-	  * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-     */
-	public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    public static final int TAG_LENS_FIRMWARE_VERSION = 0x0060;
+    public static final int TAG_BURST_SPEED = 0x0077;
+    public static final int TAG_INTELLIGENT_D_RANGE = 0x0079;
+    public static final int TAG_CLEAR_RETOUCH = 0x007c;
+    public static final int TAG_CITY2 = 0x0080;
+    public static final int TAG_PHOTO_STYLE = 0x0089;
+    public static final int TAG_SHADING_COMPENSATION = 0x008a;
+
+    public static final int TAG_ACCELEROMETER_Z = 0x008c;
+    public static final int TAG_ACCELEROMETER_X = 0x008d;
+    public static final int TAG_ACCELEROMETER_Y = 0x008e;
+    public static final int TAG_CAMERA_ORIENTATION = 0x008f;
+    public static final int TAG_ROLL_ANGLE = 0x0090;
+    public static final int TAG_PITCH_ANGLE = 0x0091;
+    public static final int TAG_SWEEP_PANORAMA_DIRECTION = 0x0093;
+    public static final int TAG_SWEEP_PANORAMA_FIELD_OF_VIEW = 0x0094;
+    public static final int TAG_TIMER_RECORDING = 0x0096;
+
+    public static final int TAG_INTERNAL_ND_FILTER = 0x009d;
+    public static final int TAG_HDR = 0x009e;
+    public static final int TAG_SHUTTER_TYPE = 0x009f;
+
+    public static final int TAG_CLEAR_RETOUCH_VALUE = 0x00a3;
+    public static final int TAG_TOUCH_AE = 0x00ab;
+
+    /**
+    * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+    */
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
     /**
      * Byte Indexes:                                                                       <br>
@@ -432,9 +467,9 @@ public class PanasonicMakernoteDirectory extends Directory
     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_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;
@@ -453,8 +488,8 @@ public class PanasonicMakernoteDirectory extends Directory
     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;
+    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)
@@ -504,43 +539,76 @@ public class PanasonicMakernoteDirectory extends Directory
         _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_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_COLOR_TEMP_KELVIN, "Color Temp Kelvin");
+        _tagNameMap.put(TAG_BRACKET_SETTINGS, "Bracket Settings");
         _tagNameMap.put(TAG_WB_ADJUST_AB, "White Balance Adjust (AB)");
-		_tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)");
-		_tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position");
+	_tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)");
+
+        _tagNameMap.put(TAG_FLASH_CURTAIN, "Flash Curtain");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_PANASONIC_IMAGE_WIDTH, "Panasonic Image Width");
+        _tagNameMap.put(TAG_PANASONIC_IMAGE_HEIGHT, "Panasonic Image Height");
+
+        _tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position");
         _tagNameMap.put(TAG_FACE_DETECTION_INFO, "Face Detection Info");
         _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
         _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
         _tagNameMap.put(TAG_ACCESSORY_TYPE, "Accessory Type");
+        _tagNameMap.put(TAG_ACCESSORY_SERIAL_NUMBER, "Accessory Serial Number");
         _tagNameMap.put(TAG_TRANSFORM, "Transform");
         _tagNameMap.put(TAG_INTELLIGENT_EXPOSURE, "Intelligent Exposure");
+        _tagNameMap.put(TAG_LENS_FIRMWARE_VERSION, "Lens Firmware Version");
         _tagNameMap.put(TAG_FACE_RECOGNITION_INFO, "Face Recognition Info");
         _tagNameMap.put(TAG_FLASH_WARNING, "Flash Warning");
         _tagNameMap.put(TAG_RECOGNIZED_FACE_FLAGS, "Recognized Face Flags");
-		_tagNameMap.put(TAG_TITLE, "Title");
-		_tagNameMap.put(TAG_BABY_NAME, "Baby Name");
-		_tagNameMap.put(TAG_LOCATION, "Location");
-		_tagNameMap.put(TAG_COUNTRY, "Country");
+        _tagNameMap.put(TAG_TITLE, "Title");
+        _tagNameMap.put(TAG_BABY_NAME, "Baby Name");
+        _tagNameMap.put(TAG_LOCATION, "Location");
+        _tagNameMap.put(TAG_COUNTRY, "Country");
         _tagNameMap.put(TAG_STATE, "State");
         _tagNameMap.put(TAG_CITY, "City");
         _tagNameMap.put(TAG_LANDMARK, "Landmark");
         _tagNameMap.put(TAG_INTELLIGENT_RESOLUTION, "Intelligent Resolution");
+        _tagNameMap.put(TAG_BURST_SPEED, "Burst Speed");
+        _tagNameMap.put(TAG_INTELLIGENT_D_RANGE, "Intelligent D-Range");
+        _tagNameMap.put(TAG_CLEAR_RETOUCH, "Clear Retouch");
+        _tagNameMap.put(TAG_CITY2, "City 2");
+        _tagNameMap.put(TAG_PHOTO_STYLE, "Photo Style");
+        _tagNameMap.put(TAG_SHADING_COMPENSATION, "Shading Compensation");
+
+        _tagNameMap.put(TAG_ACCELEROMETER_Z, "Accelerometer Z");
+        _tagNameMap.put(TAG_ACCELEROMETER_X, "Accelerometer X");
+        _tagNameMap.put(TAG_ACCELEROMETER_Y, "Accelerometer Y");
+        _tagNameMap.put(TAG_CAMERA_ORIENTATION, "Camera Orientation");
+        _tagNameMap.put(TAG_ROLL_ANGLE, "Roll Angle");
+        _tagNameMap.put(TAG_PITCH_ANGLE, "Pitch Angle");
+        _tagNameMap.put(TAG_SWEEP_PANORAMA_DIRECTION, "Sweep Panorama Direction");
+        _tagNameMap.put(TAG_SWEEP_PANORAMA_FIELD_OF_VIEW, "Sweep Panorama Field Of View");
+        _tagNameMap.put(TAG_TIMER_RECORDING, "Timer Recording");
+
+        _tagNameMap.put(TAG_INTERNAL_ND_FILTER, "Internal ND Filter");
+        _tagNameMap.put(TAG_HDR, "HDR");
+        _tagNameMap.put(TAG_SHUTTER_TYPE, "Shutter Type");
+        _tagNameMap.put(TAG_CLEAR_RETOUCH_VALUE, "Clear Retouch Value");
+        _tagNameMap.put(TAG_TOUCH_AE, "Touch AE");
+
         _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
         _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
         _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");
+        _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()
diff --git a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
index 30889e1..40ae317 100644
--- a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ import static com.drew.metadata.exif.makernotes.PentaxMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PentaxMakernoteDescriptor extends TagDescriptor<PentaxMakernoteDirectory>
 {
     public PentaxMakernoteDescriptor(@NotNull PentaxMakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
index ee38d55..8915c62 100644
--- a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PentaxMakernoteDirectory extends Directory
 {
     /**
diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java
new file mode 100644
index 0000000..97f0190
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import static com.drew.metadata.exif.makernotes.ReconyxHyperFireMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ReconyxHyperFireMakernoteDirectory}.
+ *
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxHyperFireMakernoteDescriptor extends TagDescriptor<ReconyxHyperFireMakernoteDirectory>
+{
+    public ReconyxHyperFireMakernoteDescriptor(@NotNull ReconyxHyperFireMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_MAKERNOTE_VERSION:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_FIRMWARE_VERSION:
+                return _directory.getString(tagType);
+            case TAG_TRIGGER_MODE:
+                return _directory.getString(tagType);
+            case TAG_SEQUENCE:
+                int[] sequence = _directory.getIntArray(tagType);
+                if (sequence == null)
+                    return null;
+                return String.format("%d/%d", sequence[0], sequence[1]);
+            case TAG_EVENT_NUMBER:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_MOTION_SENSITIVITY:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_BATTERY_VOLTAGE:
+                Double value = _directory.getDoubleObject(tagType);
+                DecimalFormat formatter = new DecimalFormat("0.000");
+                return value == null ? null : formatter.format(value);
+            case TAG_DATE_TIME_ORIGINAL:
+                String date = _directory.getString(tagType);
+                try {
+                    DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+                    return parser.format(parser.parse(date));
+                } catch (ParseException e) {
+                    return null;
+                }
+            case TAG_MOON_PHASE:
+                return getIndexedDescription(tagType, "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent");
+            case TAG_AMBIENT_TEMPERATURE_FAHRENHEIT:
+            case TAG_AMBIENT_TEMPERATURE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_SERIAL_NUMBER:
+                // default is UTF_16LE
+                StringValue svalue = _directory.getStringValue(tagType);
+                if(svalue == null)
+                    return null;
+                return svalue.toString();
+            case TAG_CONTRAST:
+            case TAG_BRIGHTNESS:
+            case TAG_SHARPNESS:
+            case TAG_SATURATION:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_INFRARED_ILLUMINATOR:
+                return getIndexedDescription(tagType, "Off", "On");
+            case TAG_USER_LABEL:
+                return _directory.getString(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java
new file mode 100644
index 0000000..74dbb2e
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Reconyx HyperFire cameras.
+ *
+ * Reconyx uses a fixed makernote block.  Tag values are the byte index of the tag within the makernote.
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxHyperFireMakernoteDirectory extends Directory
+{
+    /**
+     * Version number used for identifying makernotes from Reconyx HyperFire cameras.
+     */
+    public static final int MAKERNOTE_VERSION = 61697;
+
+    public static final int TAG_MAKERNOTE_VERSION = 0;
+    public static final int TAG_FIRMWARE_VERSION = 2;
+    public static final int TAG_TRIGGER_MODE = 12;
+    public static final int TAG_SEQUENCE = 14;
+    public static final int TAG_EVENT_NUMBER = 18;
+    public static final int TAG_DATE_TIME_ORIGINAL = 22;
+    public static final int TAG_MOON_PHASE = 36;
+    public static final int TAG_AMBIENT_TEMPERATURE_FAHRENHEIT = 38;
+    public static final int TAG_AMBIENT_TEMPERATURE = 40;
+    public static final int TAG_SERIAL_NUMBER = 42;
+    public static final int TAG_CONTRAST = 72;
+    public static final int TAG_BRIGHTNESS = 74;
+    public static final int TAG_SHARPNESS = 76;
+    public static final int TAG_SATURATION = 78;
+    public static final int TAG_INFRARED_ILLUMINATOR = 80;
+    public static final int TAG_MOTION_SENSITIVITY = 82;
+    public static final int TAG_BATTERY_VOLTAGE = 84;
+    public static final int TAG_USER_LABEL = 86;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_TRIGGER_MODE, "Trigger Mode");
+        _tagNameMap.put(TAG_SEQUENCE, "Sequence");
+        _tagNameMap.put(TAG_EVENT_NUMBER, "Event Number");
+        _tagNameMap.put(TAG_DATE_TIME_ORIGINAL, "Date/Time Original");
+        _tagNameMap.put(TAG_MOON_PHASE, "Moon Phase");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, "Ambient Temperature Fahrenheit");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE, "Ambient Temperature");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_BRIGHTNESS, "Brightness");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_INFRARED_ILLUMINATOR, "Infrared Illuminator");
+        _tagNameMap.put(TAG_MOTION_SENSITIVITY, "Motion Sensitivity");
+        _tagNameMap.put(TAG_BATTERY_VOLTAGE, "Battery Voltage");
+        _tagNameMap.put(TAG_USER_LABEL, "User Label");
+    }
+
+    public ReconyxHyperFireMakernoteDirectory()
+    {
+        this.setDescriptor(new ReconyxHyperFireMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Reconyx HyperFire Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java
new file mode 100644
index 0000000..86d4e70
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
+import com.drew.metadata.TagDescriptor;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+
+import static com.drew.metadata.exif.makernotes.ReconyxUltraFireMakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link ReconyxUltraFireMakernoteDirectory}.
+ *
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxUltraFireMakernoteDescriptor extends TagDescriptor<ReconyxUltraFireMakernoteDirectory>
+{
+    public ReconyxUltraFireMakernoteDescriptor(@NotNull ReconyxUltraFireMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_LABEL:
+                return _directory.getString(tagType);
+            case TAG_MAKERNOTE_ID:
+                return String.format("0x%08X", _directory.getInteger(tagType));
+            case TAG_MAKERNOTE_SIZE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_MAKERNOTE_PUBLIC_ID:
+                return String.format("0x%08X", _directory.getInteger(tagType));
+            case TAG_MAKERNOTE_PUBLIC_SIZE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_CAMERA_VERSION:
+            case TAG_UIB_VERSION:
+            case TAG_BTL_VERSION:
+            case TAG_PEX_VERSION:
+            case TAG_EVENT_TYPE:
+                return _directory.getString(tagType);
+            case TAG_SEQUENCE:
+                int[] sequence = _directory.getIntArray(tagType);
+                if (sequence == null)
+                    return null;
+                return String.format("%d/%d", sequence[0], sequence[1]);
+            case TAG_EVENT_NUMBER:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_DATE_TIME_ORIGINAL:
+                String date = _directory.getString(tagType);
+                try {
+                    DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+                    return parser.format(parser.parse(date));
+                } catch (ParseException e) {
+                    return null;
+                }
+            /*case TAG_DAY_OF_WEEK:
+                return getIndexedDescription(tagType, CultureInfo.CurrentCulture.DateTimeFormat.DayNames);*/
+            case TAG_MOON_PHASE:
+                return getIndexedDescription(tagType, "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent");
+            case TAG_AMBIENT_TEMPERATURE_FAHRENHEIT:
+            case TAG_AMBIENT_TEMPERATURE:
+                return String.format("%d", _directory.getInteger(tagType));
+            case TAG_FLASH:
+                return getIndexedDescription(tagType, "Off", "On");
+            case TAG_BATTERY_VOLTAGE:
+                Double value = _directory.getDoubleObject(tagType);
+                DecimalFormat formatter = new DecimalFormat("0.000");
+                return value == null ? null : formatter.format(value);
+            case TAG_SERIAL_NUMBER:
+                // default is UTF_8
+                StringValue svalue = _directory.getStringValue(tagType);
+                if(svalue == null)
+                    return null;
+                return svalue.toString();
+            case TAG_USER_LABEL:
+                return _directory.getString(tagType);
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java
new file mode 100644
index 0000000..d518ba7
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Reconyx UltraFire cameras.
+ *
+ * Reconyx uses a fixed makernote block.  Tag values are the byte index of the tag within the makernote.
+ * @author Todd West http://cascadescarnivoreproject.blogspot.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class ReconyxUltraFireMakernoteDirectory extends Directory
+{
+    /**
+     * Version number used for identifying makernotes from Reconyx UltraFire cameras.
+     */
+    public static final int MAKERNOTE_ID = 0x00010000;
+
+    /**
+     * Version number used for identifying the public portion of makernotes from Reconyx UltraFire cameras.
+     */
+    public static final int MAKERNOTE_PUBLIC_ID = 0x07f10001;
+
+    public static final int TAG_LABEL = 0;
+    public static final int TAG_MAKERNOTE_ID = 10;
+    public static final int TAG_MAKERNOTE_SIZE = 14;
+    public static final int TAG_MAKERNOTE_PUBLIC_ID = 18;
+    public static final int TAG_MAKERNOTE_PUBLIC_SIZE = 22;
+    public static final int TAG_CAMERA_VERSION = 24;
+    public static final int TAG_UIB_VERSION = 31;
+    public static final int TAG_BTL_VERSION = 38;
+    public static final int TAG_PEX_VERSION = 45;
+    public static final int TAG_EVENT_TYPE = 52;
+    public static final int TAG_SEQUENCE = 53;
+    public static final int TAG_EVENT_NUMBER = 55;
+    public static final int TAG_DATE_TIME_ORIGINAL = 59;
+    public static final int TAG_DAY_OF_WEEK = 66;
+    public static final int TAG_MOON_PHASE = 67;
+    public static final int TAG_AMBIENT_TEMPERATURE_FAHRENHEIT = 68;
+    public static final int TAG_AMBIENT_TEMPERATURE = 70;
+    public static final int TAG_FLASH = 72;
+    public static final int TAG_BATTERY_VOLTAGE = 73;
+    public static final int TAG_SERIAL_NUMBER = 75;
+    public static final int TAG_USER_LABEL = 80;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_LABEL, "Makernote Label");
+        _tagNameMap.put(TAG_MAKERNOTE_ID, "Makernote ID");
+        _tagNameMap.put(TAG_MAKERNOTE_SIZE, "Makernote Size");
+        _tagNameMap.put(TAG_MAKERNOTE_PUBLIC_ID, "Makernote Public ID");
+        _tagNameMap.put(TAG_MAKERNOTE_PUBLIC_SIZE, "Makernote Public Size");
+        _tagNameMap.put(TAG_CAMERA_VERSION, "Camera Version");
+        _tagNameMap.put(TAG_UIB_VERSION, "Uib Version");
+        _tagNameMap.put(TAG_BTL_VERSION, "Btl Version");
+        _tagNameMap.put(TAG_PEX_VERSION, "Pex Version");
+        _tagNameMap.put(TAG_EVENT_TYPE, "Event Type");
+        _tagNameMap.put(TAG_SEQUENCE, "Sequence");
+        _tagNameMap.put(TAG_EVENT_NUMBER, "Event Number");
+        _tagNameMap.put(TAG_DATE_TIME_ORIGINAL, "Date/Time Original");
+        _tagNameMap.put(TAG_DAY_OF_WEEK, "Day of Week");
+        _tagNameMap.put(TAG_MOON_PHASE, "Moon Phase");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, "Ambient Temperature Fahrenheit");
+        _tagNameMap.put(TAG_AMBIENT_TEMPERATURE, "Ambient Temperature");
+        _tagNameMap.put(TAG_FLASH, "Flash");
+        _tagNameMap.put(TAG_BATTERY_VOLTAGE, "Battery Voltage");
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_USER_LABEL, "User Label");
+    }
+
+    public ReconyxUltraFireMakernoteDirectory()
+    {
+        this.setDescriptor(new ReconyxUltraFireMakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Reconyx UltraFire Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
index 46001db..ace1cbc 100644
--- a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -25,13 +25,14 @@ import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDescriptor}.
+ * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDirectory}.
  * <p>
  * Some information about this makernote taken from here:
  * http://www.ozhiker.com/electronics/pjmt/jpeg_info/ricoh_mn.html
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirectory>
 {
     public RicohMakernoteDescriptor(@NotNull RicohMakernoteDirectory directory)
@@ -44,20 +45,12 @@ public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirect
     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()
diff --git a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
index fdb1e90..da36a58 100644
--- a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class RicohMakernoteDirectory extends Directory
 {
     public static final int TAG_MAKERNOTE_DATA_TYPE = 0x0001;
diff --git a/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java
new file mode 100644
index 0000000..dc747ed
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.exif.makernotes.SamsungType2MakernoteDirectory.*;
+
+/**
+ * Provides human-readable string representations of tag values stored in a {@link SamsungType2MakernoteDirectory}.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Samsung.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class SamsungType2MakernoteDescriptor extends TagDescriptor<SamsungType2MakernoteDirectory>
+{
+    public SamsungType2MakernoteDescriptor(@NotNull SamsungType2MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TagMakerNoteVersion:
+                return getMakernoteVersionDescription();
+            case TagDeviceType:
+                return getDeviceTypeDescription();
+            case TagSamsungModelId:
+                return getSamsungModelIdDescription();
+
+            case TagCameraTemperature:
+                return getCameraTemperatureDescription();
+
+            case TagFaceDetect:
+                return getFaceDetectDescription();
+            case TagFaceRecognition:
+                return getFaceRecognitionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return getVersionBytesDescription(TagMakerNoteVersion, 2);
+    }
+
+    @Nullable
+    public String getDeviceTypeDescription()
+    {
+        Integer value = _directory.getInteger(TagDeviceType);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0x1000:
+                return "Compact Digital Camera";
+            case 0x2000:
+                return "High-end NX Camera";
+            case 0x3000:
+                return "HXM Video Camera";
+            case 0x12000:
+                return "Cell Phone";
+            case 0x300000:
+                return "SMX Video Camera";
+            default:
+                return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getSamsungModelIdDescription()
+    {
+        Integer value = _directory.getInteger(TagSamsungModelId);
+        if (value == null)
+            return null;
+
+        switch (value)
+        {
+            case 0x100101c:
+                return "NX10";
+            /*case 0x1001226:
+                    return "HMX-S10BP";*/
+            case 0x1001226:
+                    return "HMX-S15BP";
+            case 0x1001233:
+                    return "HMX-Q10";
+            /*case 0x1001234:
+                    return "HMX-H300";*/
+            case 0x1001234:
+                    return "HMX-H304";
+            case 0x100130c:
+                    return "NX100";
+            case 0x1001327:
+                    return "NX11";
+            case 0x170104e:
+                    return "ES70, ES71 / VLUU ES70, ES71 / SL600";
+            case 0x1701052:
+                    return "ES73 / VLUU ES73 / SL605";
+            case 0x1701300:
+                    return "ES28 / VLUU ES28";
+            case 0x1701303:
+                    return "ES74,ES75,ES78 / VLUU ES75,ES78";
+            case 0x2001046:
+                    return "PL150 / VLUU PL150 / TL210 / PL151";
+            case 0x2001311:
+                    return "PL120,PL121 / VLUU PL120,PL121";
+            case 0x2001315:
+                    return "PL170,PL171 / VLUUPL170,PL171";
+            case 0x200131e:
+                    return "PL210, PL211 / VLUU PL210, PL211";
+            case 0x2701317:
+                    return "PL20,PL21 / VLUU PL20,PL21";
+            case 0x2a0001b:
+                    return "WP10 / VLUU WP10 / AQ100";
+            case 0x3000000:
+                    return "Various Models (0x3000000)";
+            case 0x3a00018:
+                    return "Various Models (0x3a00018)";
+            case 0x400101f:
+                    return "ST1000 / ST1100 / VLUU ST1000 / CL65";
+            case 0x4001022:
+                    return "ST550 / VLUU ST550 / TL225";
+            case 0x4001025:
+                    return "Various Models (0x4001025)";
+            case 0x400103e:
+                    return "VLUU ST5500, ST5500, CL80";
+            case 0x4001041:
+                    return "VLUU ST5000, ST5000, TL240";
+            case 0x4001043:
+                    return "ST70 / VLUU ST70 / ST71";
+            case 0x400130a:
+                    return "Various Models (0x400130a)";
+            case 0x400130e:
+                    return "ST90,ST91 / VLUU ST90,ST91";
+            case 0x4001313:
+                    return "VLUU ST95, ST95";
+            case 0x4a00015:
+                    return "VLUU ST60";
+            case 0x4a0135b:
+                    return "ST30, ST65 / VLUU ST65 / ST67";
+            case 0x5000000:
+                    return "Various Models (0x5000000)";
+            case 0x5001038:
+                    return "Various Models (0x5001038)";
+            case 0x500103a:
+                    return "WB650 / VLUU WB650 / WB660";
+            case 0x500103c:
+                    return "WB600 / VLUU WB600 / WB610";
+            case 0x500133e:
+                    return "WB150 / WB150F / WB152 / WB152F / WB151";
+            case 0x5a0000f:
+                    return "WB5000 / HZ25W";
+            case 0x6001036:
+                    return "EX1";
+            case 0x700131c:
+                    return "VLUU SH100, SH100";
+            case 0x27127002:
+                    return "SMX - C20N";
+            default:
+                return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    private String getCameraTemperatureDescription()
+    {
+        return getFormattedInt(TagCameraTemperature, "%d C");
+    }
+
+    @Nullable
+    public String getFaceDetectDescription()
+    {
+        return getIndexedDescription(TagFaceDetect,
+            "Off", "On");
+    }
+
+    @Nullable
+    public String getFaceRecognitionDescription()
+    {
+        return getIndexedDescription(TagFaceRecognition,
+            "Off", "On");
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java
new file mode 100644
index 0000000..5af173c
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif.makernotes;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific certain 'newer' Samsung cameras.
+ * <p>
+ * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Samsung.html
+ *
+ * @author Kevin Mott https://github.com/kwhopper
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class SamsungType2MakernoteDirectory extends Directory
+{
+    // This list is incomplete
+    public static final int TagMakerNoteVersion = 0x001;
+    public static final int TagDeviceType = 0x0002;
+    public static final int TagSamsungModelId = 0x0003;
+
+    public static final int TagCameraTemperature = 0x0043;
+
+    public static final int TagFaceDetect = 0x0100;
+    public static final int TagFaceRecognition = 0x0120;
+    public static final int TagFaceName = 0x0123;
+
+    // following tags found only in SRW images
+    public static final int TagFirmwareName = 0xa001;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TagMakerNoteVersion, "Maker Note Version");
+        _tagNameMap.put(TagDeviceType, "Device Type");
+        _tagNameMap.put(TagSamsungModelId, "Model Id");
+
+        _tagNameMap.put(TagCameraTemperature, "Camera Temperature");
+
+        _tagNameMap.put(TagFaceDetect, "Face Detect");
+        _tagNameMap.put(TagFaceRecognition, "Face Recognition");
+        _tagNameMap.put(TagFaceName, "Face Name");
+        _tagNameMap.put(TagFirmwareName, "Firmware Name");
+    }
+
+    public SamsungType2MakernoteDirectory()
+    {
+        this.setDescriptor(new SamsungType2MakernoteDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Samsung Makernote";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
index 22db4fe..3367fa1 100644
--- a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SanyoMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SanyoMakernoteDescriptor extends TagDescriptor<SanyoMakernoteDirectory>
 {
     public SanyoMakernoteDescriptor(@NotNull SanyoMakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
index 26500c6..e22d1fb 100644
--- a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SanyoMakernoteDirectory extends Directory
 {
     public static final int TAG_MAKERNOTE_OFFSET = 0x00ff;
@@ -61,7 +62,7 @@ public class SanyoMakernoteDirectory extends Directory
     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_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
     public static final int TAG_DATA_DUMP = 0x0f00;
 
@@ -98,7 +99,7 @@ public class SanyoMakernoteDirectory extends Directory
         _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_PRINT_IMAGE_MATCHING_INFO, "Print IM");
 
         _tagNameMap.put(TAG_DATA_DUMP, "Data Dump");
     }
diff --git a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
index 56442f1..3111378 100644
--- a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SigmaMakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SigmaMakernoteDescriptor extends TagDescriptor<SigmaMakernoteDirectory>
 {
     public SigmaMakernoteDescriptor(@NotNull SigmaMakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
index 75afedf..c62b002 100644
--- a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SigmaMakernoteDirectory extends Directory
 {
     public static final int TAG_SERIAL_NUMBER = 0x2;
diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
index 8ae873f..12e383b 100644
--- a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SonyType1MakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1MakernoteDirectory>
 {
     public SonyType1MakernoteDescriptor(@NotNull SonyType1MakernoteDirectory directory)
@@ -432,19 +433,43 @@ public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1Makerno
     @Nullable
     public String getVignettingCorrectionDescription()
     {
-        return getIndexedDescription(TAG_VIGNETTING_CORRECTION, "Off", null, "Auto");
+        Integer value = _directory.getInteger(TAG_VIGNETTING_CORRECTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 2: return "Auto";
+            case 0xffffffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
     }
 
     @Nullable
     public String getLateralChromaticAberrationDescription()
     {
-        return getIndexedDescription(TAG_LATERAL_CHROMATIC_ABERRATION, "Off", null, "Auto");
+        Integer value = _directory.getInteger(TAG_LATERAL_CHROMATIC_ABERRATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 2: return "Auto";
+            case 0xffffffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
     }
 
     @Nullable
     public String getDistortionCorrectionDescription()
     {
-        return getIndexedDescription(TAG_DISTORTION_CORRECTION, "Off", null, "Auto");
+        Integer value = _directory.getInteger(TAG_DISTORTION_CORRECTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 2: return "Auto";
+            case 0xffffffff: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
     }
 
     @Nullable
diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
index ef5ce0d..2a23ffb 100644
--- a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType1MakernoteDirectory extends Directory
 {
     public static final int TAG_CAMERA_INFO = 0x0010;
@@ -140,7 +141,7 @@ public class SonyType1MakernoteDirectory extends Directory
         _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_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
 
         _tagNameMap.put(TAG_MULTI_BURST_MODE, "Multi Burst Mode");
         _tagNameMap.put(TAG_MULTI_BURST_IMAGE_WIDTH, "Multi Burst Image Width");
diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
index 8175dec..c08f33a 100644
--- a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
+++ b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType6MakernoteDescriptor extends TagDescriptor<SonyType6MakernoteDirectory>
 {
     public SonyType6MakernoteDescriptor(@NotNull SonyType6MakernoteDirectory directory)
diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
index a68d81e..90690a1 100644
--- a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
+++ b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class SonyType6MakernoteDirectory extends Directory
 {
     public static final int TAG_MAKERNOTE_THUMB_OFFSET = 0x0513;
diff --git a/Source/com/drew/metadata/exif/makernotes/package-info.java b/Source/com/drew/metadata/exif/makernotes/package-info.java
new file mode 100644
index 0000000..a3bc025
--- /dev/null
+++ b/Source/com/drew/metadata/exif/makernotes/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the
+ * modelling of manufacturer-specific makernotes.
+ */
+package com.drew.metadata.exif.makernotes;
diff --git a/Source/com/drew/metadata/exif/makernotes/package.html b/Source/com/drew/metadata/exif/makernotes/package.html
deleted file mode 100644
index 7115326..0000000
--- a/Source/com/drew/metadata/exif/makernotes/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/metadata/exif/package-info.java b/Source/com/drew/metadata/exif/package-info.java
new file mode 100644
index 0000000..707b8d6
--- /dev/null
+++ b/Source/com/drew/metadata/exif/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes.
+ */
+package com.drew.metadata.exif;
diff --git a/Source/com/drew/metadata/exif/package.html b/Source/com/drew/metadata/exif/package.html
deleted file mode 100644
index 0ec6f41..0000000
--- a/Source/com/drew/metadata/exif/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/metadata/file/FileMetadataDescriptor.java b/Source/com/drew/metadata/file/FileMetadataDescriptor.java
new file mode 100644
index 0000000..601ea7d
--- /dev/null
+++ b/Source/com/drew/metadata/file/FileMetadataDescriptor.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.file.FileMetadataDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class FileMetadataDescriptor extends TagDescriptor<FileMetadataDirectory>
+{
+    public FileMetadataDescriptor(@NotNull FileMetadataDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_FILE_SIZE:
+                return getFileSizeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getFileSizeDescription()
+    {
+        Long size = _directory.getLongObject(TAG_FILE_SIZE);
+
+        if (size == null)
+            return null;
+
+        return Long.toString(size) + " bytes";
+    }
+}
+
diff --git a/Source/com/drew/metadata/file/FileMetadataDirectory.java b/Source/com/drew/metadata/file/FileMetadataDirectory.java
new file mode 100644
index 0000000..f01d832
--- /dev/null
+++ b/Source/com/drew/metadata/file/FileMetadataDirectory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class FileMetadataDirectory extends Directory
+{
+    public static final int TAG_FILE_NAME = 1;
+    public static final int TAG_FILE_SIZE = 2;
+    public static final int TAG_FILE_MODIFIED_DATE = 3;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_FILE_NAME, "File Name");
+        _tagNameMap.put(TAG_FILE_SIZE, "File Size");
+        _tagNameMap.put(TAG_FILE_MODIFIED_DATE, "File Modified Date");
+    }
+
+    public FileMetadataDirectory()
+    {
+        this.setDescriptor(new FileMetadataDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "File";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/file/FileMetadataReader.java b/Source/com/drew/metadata/file/FileMetadataReader.java
new file mode 100644
index 0000000..8cb5135
--- /dev/null
+++ b/Source/com/drew/metadata/file/FileMetadataReader.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+
+public class FileMetadataReader
+{
+    public void read(@NotNull File file, @NotNull Metadata metadata) throws IOException
+    {
+        if (!file.isFile())
+            throw new IOException("File object must reference a file");
+        if (!file.exists())
+            throw new IOException("File does not exist");
+        if (!file.canRead())
+            throw new IOException("File is not readable");
+
+        FileMetadataDirectory directory = new FileMetadataDirectory();
+
+        directory.setString(FileMetadataDirectory.TAG_FILE_NAME, file.getName());
+        directory.setLong(FileMetadataDirectory.TAG_FILE_SIZE, file.length());
+        directory.setDate(FileMetadataDirectory.TAG_FILE_MODIFIED_DATE, new Date(file.lastModified()));
+
+        metadata.addDirectory(directory);
+    }
+}
diff --git a/Source/com/drew/metadata/file/package-info.java b/Source/com/drew/metadata/file/package-info.java
new file mode 100644
index 0000000..d8c0b47
--- /dev/null
+++ b/Source/com/drew/metadata/file/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of file system metadata.
+ *
+ * @since 2.8.0
+ */
+package com.drew.metadata.file;
diff --git a/Source/com/drew/metadata/gif/GifAnimationDescriptor.java b/Source/com/drew/metadata/gif/GifAnimationDescriptor.java
new file mode 100644
index 0000000..69315ec
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifAnimationDescriptor.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.gif.GifAnimationDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifAnimationDescriptor extends TagDescriptor<GifAnimationDirectory>
+{
+    public GifAnimationDescriptor(@NotNull GifAnimationDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_ITERATION_COUNT:
+                return getIterationCountDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getIterationCountDescription()
+    {
+        Integer count = _directory.getInteger(TAG_ITERATION_COUNT);
+        if (count == null)
+            return null;
+
+        return count == 0 ? "Infinite" : count == 1 ? "Once" : count == 2 ? "Twice" : count.toString() + " times";
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifAnimationDirectory.java b/Source/com/drew/metadata/gif/GifAnimationDirectory.java
new file mode 100644
index 0000000..7e3734c
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifAnimationDirectory.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifAnimationDirectory extends Directory
+{
+    public static final int TAG_ITERATION_COUNT = 1;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_ITERATION_COUNT, "Iteration Count");
+    }
+
+    public GifAnimationDirectory()
+    {
+        this.setDescriptor(new GifAnimationDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "GIF Animation";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifCommentDescriptor.java b/Source/com/drew/metadata/gif/GifCommentDescriptor.java
new file mode 100644
index 0000000..06f2028
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifCommentDescriptor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifCommentDescriptor extends TagDescriptor<GifCommentDirectory>
+{
+    public GifCommentDescriptor(@NotNull GifCommentDirectory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifCommentDirectory.java b/Source/com/drew/metadata/gif/GifCommentDirectory.java
new file mode 100644
index 0000000..eaa4b3e
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifCommentDirectory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.StringValue;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifCommentDirectory extends Directory
+{
+    public static final int TAG_COMMENT = 1;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_COMMENT, "Comment");
+    }
+
+    public GifCommentDirectory(StringValue comment)
+    {
+        this.setDescriptor(new GifCommentDescriptor(this));
+        setStringValue(TAG_COMMENT, comment);
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "GIF Comment";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifControlDescriptor.java b/Source/com/drew/metadata/gif/GifControlDescriptor.java
new file mode 100644
index 0000000..8e4cf15
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifControlDescriptor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifControlDescriptor extends TagDescriptor<GifControlDirectory>
+{
+    public GifControlDescriptor(@NotNull GifControlDirectory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifControlDirectory.java b/Source/com/drew/metadata/gif/GifControlDirectory.java
new file mode 100644
index 0000000..5b98f5c
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifControlDirectory.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifControlDirectory extends Directory
+{
+    public static final int TAG_DELAY = 1;
+    public static final int TAG_DISPOSAL_METHOD = 2;
+    public static final int TAG_USER_INPUT_FLAG = 3;
+    public static final int TAG_TRANSPARENT_COLOR_FLAG = 4;
+    public static final int TAG_TRANSPARENT_COLOR_INDEX = 5;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_DELAY, "Delay");
+        _tagNameMap.put(TAG_DISPOSAL_METHOD, "Disposal Method");
+        _tagNameMap.put(TAG_USER_INPUT_FLAG, "User Input Flag");
+        _tagNameMap.put(TAG_TRANSPARENT_COLOR_FLAG, "Transparent Color Flag");
+        _tagNameMap.put(TAG_TRANSPARENT_COLOR_INDEX, "Transparent Color Index");
+    }
+
+    public GifControlDirectory()
+    {
+        this.setDescriptor(new GifControlDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "GIF Control";
+    }
+
+    /**
+     * @return The {@link DisposalMethod}.
+     */
+    @NotNull
+    public DisposalMethod getDisposalMethod() {
+        return (DisposalMethod) getObject(TAG_DISPOSAL_METHOD);
+    }
+
+    /**
+     * @return Whether the GIF has transparency.
+     */
+    public boolean isTransparent() {
+        Boolean transparent = getBooleanObject(TAG_TRANSPARENT_COLOR_FLAG);
+        return transparent != null ? transparent.booleanValue() : false;
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * Disposal method indicates the way in which the graphic is to be treated
+     * after being displayed.
+     */
+    public enum DisposalMethod {
+        NOT_SPECIFIED,
+        DO_NOT_DISPOSE,
+        RESTORE_TO_BACKGROUND_COLOR,
+        RESTORE_TO_PREVIOUS,
+        TO_BE_DEFINED,
+        INVALID;
+
+        public static DisposalMethod typeOf(int value) {
+            switch (value) {
+                case 0: return NOT_SPECIFIED;
+                case 1: return DO_NOT_DISPOSE;
+                case 2: return RESTORE_TO_BACKGROUND_COLOR;
+                case 3: return RESTORE_TO_PREVIOUS;
+                case 4:
+                case 5:
+                case 6:
+                case 7: return TO_BE_DEFINED;
+                default: return INVALID;
+            }
+        }
+
+        @Override
+        public String toString() {
+            switch (this) {
+                case DO_NOT_DISPOSE:
+                    return "Don't Dispose";
+                case INVALID:
+                    return "Invalid value";
+                case NOT_SPECIFIED:
+                    return "Not Specified";
+                case RESTORE_TO_BACKGROUND_COLOR:
+                    return "Restore to Background Color";
+                case RESTORE_TO_PREVIOUS:
+                    return "Restore to Previous";
+                case TO_BE_DEFINED:
+                    return "To Be Defined";
+                default:
+                    return super.toString();
+            }
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifHeaderDescriptor.java b/Source/com/drew/metadata/gif/GifHeaderDescriptor.java
index b32abbe..c5fb5c0 100644
--- a/Source/com/drew/metadata/gif/GifHeaderDescriptor.java
+++ b/Source/com/drew/metadata/gif/GifHeaderDescriptor.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.gif;
 
 import com.drew.lang.annotations.NotNull;
@@ -6,6 +26,7 @@ import com.drew.metadata.TagDescriptor;
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class GifHeaderDescriptor extends TagDescriptor<GifHeaderDirectory>
 {
     public GifHeaderDescriptor(@NotNull GifHeaderDirectory directory)
diff --git a/Source/com/drew/metadata/gif/GifHeaderDirectory.java b/Source/com/drew/metadata/gif/GifHeaderDirectory.java
index b0e436e..ec4bc80 100644
--- a/Source/com/drew/metadata/gif/GifHeaderDirectory.java
+++ b/Source/com/drew/metadata/gif/GifHeaderDirectory.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.gif;
 
 import com.drew.lang.annotations.NotNull;
@@ -8,6 +28,7 @@ import java.util.HashMap;
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class GifHeaderDirectory extends Directory
 {
     public static final int TAG_GIF_FORMAT_VERSION = 1;
@@ -17,7 +38,12 @@ public class GifHeaderDirectory extends Directory
     public static final int TAG_IS_COLOR_TABLE_SORTED = 5;
     public static final int TAG_BITS_PER_PIXEL = 6;
     public static final int TAG_HAS_GLOBAL_COLOR_TABLE = 7;
+    /**
+     * @deprecated use {@link #TAG_BACKGROUND_COLOR_INDEX} instead.
+     */
+    @Deprecated
     public static final int TAG_TRANSPARENT_COLOR_INDEX = 8;
+    public static final int TAG_BACKGROUND_COLOR_INDEX = 8;
     public static final int TAG_PIXEL_ASPECT_RATIO = 9;
 
     @NotNull
@@ -31,7 +57,7 @@ public class GifHeaderDirectory extends Directory
         _tagNameMap.put(TAG_IS_COLOR_TABLE_SORTED, "Is Color Table Sorted");
         _tagNameMap.put(TAG_BITS_PER_PIXEL, "Bits per Pixel");
         _tagNameMap.put(TAG_HAS_GLOBAL_COLOR_TABLE, "Has Global Color Table");
-        _tagNameMap.put(TAG_TRANSPARENT_COLOR_INDEX, "Transparent Color Index");
+        _tagNameMap.put(TAG_BACKGROUND_COLOR_INDEX, "Background Color Index");
         _tagNameMap.put(TAG_PIXEL_ASPECT_RATIO, "Pixel Aspect Ratio");
     }
 
diff --git a/Source/com/drew/metadata/gif/GifImageDescriptor.java b/Source/com/drew/metadata/gif/GifImageDescriptor.java
new file mode 100644
index 0000000..7fff4bf
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifImageDescriptor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifImageDescriptor extends TagDescriptor<GifImageDirectory>
+{
+    public GifImageDescriptor(@NotNull GifImageDirectory directory)
+    {
+        super(directory);
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifImageDirectory.java b/Source/com/drew/metadata/gif/GifImageDirectory.java
new file mode 100644
index 0000000..bffa2ff
--- /dev/null
+++ b/Source/com/drew/metadata/gif/GifImageDirectory.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.gif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
+ */
+@SuppressWarnings("WeakerAccess")
+public class GifImageDirectory extends Directory
+{
+    public static final int TAG_LEFT = 1;
+    public static final int TAG_TOP = 2;
+    public static final int TAG_WIDTH = 3;
+    public static final int TAG_HEIGHT = 4;
+    public static final int TAG_HAS_LOCAL_COLOUR_TABLE = 5;
+    public static final int TAG_IS_INTERLACED = 6;
+    public static final int TAG_IS_COLOR_TABLE_SORTED = 7;
+    public static final int TAG_LOCAL_COLOUR_TABLE_BITS_PER_PIXEL = 8;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_LEFT, "Left");
+        _tagNameMap.put(TAG_TOP, "Top");
+        _tagNameMap.put(TAG_WIDTH, "Width");
+        _tagNameMap.put(TAG_HEIGHT, "Height");
+        _tagNameMap.put(TAG_HAS_LOCAL_COLOUR_TABLE, "Has Local Colour Table");
+        _tagNameMap.put(TAG_IS_INTERLACED, "Is Interlaced");
+        _tagNameMap.put(TAG_IS_COLOR_TABLE_SORTED, "Is Local Colour Table Sorted");
+        _tagNameMap.put(TAG_LOCAL_COLOUR_TABLE_BITS_PER_PIXEL, "Local Colour Table Bits Per Pixel");
+    }
+
+    public GifImageDirectory()
+    {
+        this.setDescriptor(new GifImageDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "GIF Image";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/gif/GifReader.java b/Source/com/drew/metadata/gif/GifReader.java
index e313bdb..5ff4ad7 100644
--- a/Source/com/drew/metadata/gif/GifReader.java
+++ b/Source/com/drew/metadata/gif/GifReader.java
@@ -1,13 +1,54 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.gif;
 
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.Charsets;
 import com.drew.lang.SequentialReader;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import com.drew.metadata.Directory;
+import com.drew.metadata.ErrorDirectory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
+import com.drew.metadata.gif.GifControlDirectory.DisposalMethod;
+import com.drew.metadata.icc.IccReader;
+import com.drew.metadata.xmp.XmpReader;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 
 /**
+ * Reader of GIF encoded data.
+ *
+ * Resources:
+ * <ul>
+ *     <li>https://wiki.whatwg.org/wiki/GIF</li>
+ *     <li>https://www.w3.org/Graphics/GIF/spec-gif89a.txt</li>
+ *     <li>http://web.archive.org/web/20100929230301/http://www.etsimo.uniovi.es/gifanim/gif87a.txt</li>
+ * </ul>
+ *
  * @author Drew Noakes https://drewnoakes.com
+ * @author Kevin Mott https://github.com/kwhopper
  */
 public class GifReader
 {
@@ -16,8 +57,75 @@ public class GifReader
 
     public void extract(@NotNull final SequentialReader reader, final @NotNull Metadata metadata)
     {
-        final GifHeaderDirectory directory = metadata.getOrCreateDirectory(GifHeaderDirectory.class);
+        reader.setMotorolaByteOrder(false);
+
+        GifHeaderDirectory header;
+        try {
+            header = readGifHeader(reader);
+            metadata.addDirectory(header);
+        } catch (IOException ex) {
+            metadata.addDirectory(new ErrorDirectory("IOException processing GIF data"));
+            return;
+        }
+
+        if(header.hasErrors())
+            return;
+
+        try {
+            // Skip over any global colour table
+            Integer globalColorTableSize = header.getInteger(GifHeaderDirectory.TAG_COLOR_TABLE_SIZE);
+            if (globalColorTableSize != null)
+            {
+                // Colour table has R/G/B byte triplets
+                reader.skip(3 * globalColorTableSize);
+            }
+
+            // After the header comes a sequence of blocks
+            while (true)
+            {
+                byte marker;
+                try {
+                    marker = reader.getInt8();
+                } catch (IOException ex) {
+                    return;
+                }
 
+                switch (marker)
+                {
+                    case (byte)'!': // 0x21
+                    {
+                        readGifExtensionBlock(reader, metadata);
+                        break;
+                    }
+                    case (byte)',': // 0x2c
+                    {
+                        metadata.addDirectory(readImageBlock(reader));
+
+                        // skip image data blocks
+                        skipBlocks(reader);
+                        break;
+                    }
+                    case (byte)';': // 0x3b
+                    {
+                        // terminator
+                        return;
+                    }
+                    default:
+                    {
+                        // Anything other than these types is unexpected.
+                        // GIF87a spec says to keep reading until a separator is found.
+                        // GIF89a spec says file is corrupt.
+                        return;
+                    }
+                }
+            }
+        } catch (IOException e) {
+            metadata.addDirectory(new ErrorDirectory("IOException processing GIF data"));
+        }
+    }
+
+    private static GifHeaderDirectory readGifHeader(@NotNull final SequentialReader reader) throws IOException
+    {
         // FILE HEADER
         //
         // 3 - signature: "GIF"
@@ -35,55 +143,254 @@ public class GifReader
         // 1 - background color index
         // 1 - pixel aspect ratio
 
-        reader.setMotorolaByteOrder(false);
+        GifHeaderDirectory headerDirectory = new GifHeaderDirectory();
 
-        try {
-            String signature = reader.getString(3);
+        String signature = reader.getString(3);
 
-            if (!signature.equals("GIF"))
-            {
-                directory.addError("Invalid GIF file signature");
-                return;
-            }
+        if (!signature.equals("GIF"))
+        {
+            headerDirectory.addError("Invalid GIF file signature");
+            return headerDirectory;
+        }
 
-            String version = reader.getString(3);
+        String version = reader.getString(3);
 
-            if (!version.equals(GIF_87A_VERSION_IDENTIFIER) && !version.equals(GIF_89A_VERSION_IDENTIFIER)) {
-                directory.addError("Unexpected GIF version");
-                return;
-            }
+        if (!version.equals(GIF_87A_VERSION_IDENTIFIER) && !version.equals(GIF_89A_VERSION_IDENTIFIER)) {
+            headerDirectory.addError("Unexpected GIF version");
+            return headerDirectory;
+        }
 
-            directory.setString(GifHeaderDirectory.TAG_GIF_FORMAT_VERSION, version);
-            directory.setInt(GifHeaderDirectory.TAG_IMAGE_WIDTH, reader.getUInt16());
-            directory.setInt(GifHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
+        headerDirectory.setString(GifHeaderDirectory.TAG_GIF_FORMAT_VERSION, version);
 
-            short flags = reader.getUInt8();
+        // LOGICAL SCREEN DESCRIPTOR
 
-            // First three bits = (BPP - 1)
-            int colorTableSize = 1 << ((flags & 7) + 1);
-            directory.setInt(GifHeaderDirectory.TAG_COLOR_TABLE_SIZE, colorTableSize);
+        headerDirectory.setInt(GifHeaderDirectory.TAG_IMAGE_WIDTH, reader.getUInt16());
+        headerDirectory.setInt(GifHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
 
-            if (version.equals(GIF_89A_VERSION_IDENTIFIER)) {
-                boolean isColorTableSorted = (flags & 8) != 0;
-                directory.setBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED, isColorTableSorted);
-            }
+        short flags = reader.getUInt8();
 
-            int bitsPerPixel = ((flags & 0x70) >> 4) + 1;
-            directory.setInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL, bitsPerPixel);
+        // First three bits = (BPP - 1)
+        int colorTableSize = 1 << ((flags & 7) + 1);
+        int bitsPerPixel = ((flags & 0x70) >> 4) + 1;
+        boolean hasGlobalColorTable = (flags & 0xf) != 0;
 
-            boolean hasGlobalColorTable = (flags & 0xf) != 0;
-            directory.setBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE, hasGlobalColorTable);
+        headerDirectory.setInt(GifHeaderDirectory.TAG_COLOR_TABLE_SIZE, colorTableSize);
 
-            directory.setInt(GifHeaderDirectory.TAG_TRANSPARENT_COLOR_INDEX, reader.getUInt8());
+        if (version.equals(GIF_89A_VERSION_IDENTIFIER)) {
+            boolean isColorTableSorted = (flags & 8) != 0;
+            headerDirectory.setBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED, isColorTableSorted);
+        }
 
-            int aspectRatioByte = reader.getUInt8();
-            if (aspectRatioByte != 0) {
-                float pixelAspectRatio = (float)((aspectRatioByte + 15d) / 64d);
-                directory.setFloat(GifHeaderDirectory.TAG_PIXEL_ASPECT_RATIO, pixelAspectRatio);
-            }
+        headerDirectory.setInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL, bitsPerPixel);
+        headerDirectory.setBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE, hasGlobalColorTable);
 
-        } catch (IOException e) {
-            directory.addError("Unable to read BMP header");
+        headerDirectory.setInt(GifHeaderDirectory.TAG_BACKGROUND_COLOR_INDEX, reader.getUInt8());
+
+        int aspectRatioByte = reader.getUInt8();
+        if (aspectRatioByte != 0) {
+            float pixelAspectRatio = (float)((aspectRatioByte + 15d) / 64d);
+            headerDirectory.setFloat(GifHeaderDirectory.TAG_PIXEL_ASPECT_RATIO, pixelAspectRatio);
+        }
+
+        return headerDirectory;
+    }
+
+    private static void readGifExtensionBlock(SequentialReader reader, Metadata metadata) throws IOException
+    {
+        byte extensionLabel = reader.getInt8();
+        short blockSizeBytes = reader.getUInt8();
+        long blockStartPos = reader.getPosition();
+
+        switch (extensionLabel)
+        {
+            case (byte) 0x01:
+                Directory plainTextBlock = readPlainTextBlock(reader, blockSizeBytes);
+                if (plainTextBlock != null)
+                    metadata.addDirectory(plainTextBlock);
+                break;
+            case (byte) 0xf9:
+                metadata.addDirectory(readControlBlock(reader, blockSizeBytes));
+                break;
+            case (byte) 0xfe:
+                metadata.addDirectory(readCommentBlock(reader, blockSizeBytes));
+                break;
+            case (byte) 0xff:
+                readApplicationExtensionBlock(reader, blockSizeBytes, metadata);
+                break;
+            default:
+                metadata.addDirectory(new ErrorDirectory(String.format("Unsupported GIF extension block with type 0x%02X.", extensionLabel)));
+                break;
+        }
+
+        long skipCount = blockStartPos + blockSizeBytes - reader.getPosition();
+        if (skipCount > 0)
+            reader.skip(skipCount);
+    }
+
+    @Nullable
+    private static Directory readPlainTextBlock(SequentialReader reader, int blockSizeBytes) throws IOException
+    {
+        // It seems this extension is deprecated. If somebody finds an image with this in it, could implement here.
+        // Just skip the entire block for now.
+
+        if (blockSizeBytes != 12)
+            return new ErrorDirectory(String.format("Invalid GIF plain text block size. Expected 12, got %d.", blockSizeBytes));
+
+        // skip 'blockSizeBytes' bytes
+        reader.skip(12);
+
+        // keep reading and skipping until a 0 byte is reached
+        skipBlocks(reader);
+
+        return null;
+    }
+
+    private static GifCommentDirectory readCommentBlock(SequentialReader reader, int blockSizeBytes) throws IOException
+    {
+        byte[] buffer = gatherBytes(reader, blockSizeBytes);
+        return new GifCommentDirectory(new StringValue(buffer, Charsets.ASCII));
+    }
+
+    private static void readApplicationExtensionBlock(SequentialReader reader, int blockSizeBytes, Metadata metadata) throws IOException
+    {
+        if (blockSizeBytes != 11)
+        {
+            metadata.addDirectory(new ErrorDirectory(String.format("Invalid GIF application extension block size. Expected 11, got %d.", blockSizeBytes)));
+            return;
+        }
+
+        String extensionType = reader.getString(blockSizeBytes, Charsets.UTF_8);
+
+        if (extensionType.equals("XMP DataXMP"))
+        {
+            // XMP data extension
+            byte[] xmpBytes = gatherBytes(reader);
+            new XmpReader().extract(xmpBytes, 0, xmpBytes.length - 257, metadata, null);
+        }
+        else if (extensionType.equals("ICCRGBG1012"))
+        {
+            // ICC profile extension
+            byte[] iccBytes = gatherBytes(reader, reader.getByte());
+            if (iccBytes.length != 0)
+                new IccReader().extract(new ByteArrayReader(iccBytes), metadata);
+        }
+        else if (extensionType.equals("NETSCAPE2.0"))
+        {
+            reader.skip(2);
+            // Netscape's animated GIF extension
+            // Iteration count (0 means infinite)
+            int iterationCount = reader.getUInt16();
+            // Skip terminator
+            reader.skip(1);
+            GifAnimationDirectory animationDirectory = new GifAnimationDirectory();
+            animationDirectory.setInt(GifAnimationDirectory.TAG_ITERATION_COUNT, iterationCount);
+            metadata.addDirectory(animationDirectory);
+        }
+        else
+        {
+            skipBlocks(reader);
+        }
+    }
+
+    private static GifControlDirectory readControlBlock(SequentialReader reader, int blockSizeBytes) throws IOException
+    {
+        if (blockSizeBytes < 4)
+            blockSizeBytes = 4;
+
+        GifControlDirectory directory = new GifControlDirectory();
+
+        short packedFields = reader.getUInt8();
+        directory.setObject(GifControlDirectory.TAG_DISPOSAL_METHOD, DisposalMethod.typeOf((packedFields >> 2) & 7));
+        directory.setBoolean(GifControlDirectory.TAG_USER_INPUT_FLAG, (packedFields & 2) >> 1 == 1 ? true : false);
+        directory.setBoolean(GifControlDirectory.TAG_TRANSPARENT_COLOR_FLAG, (packedFields & 1) == 1 ? true : false);
+        directory.setInt(GifControlDirectory.TAG_DELAY, reader.getUInt16());
+        directory.setInt(GifControlDirectory.TAG_TRANSPARENT_COLOR_INDEX, reader.getUInt8());
+
+        // skip 0x0 block terminator
+        reader.skip(1);
+
+        return directory;
+    }
+
+    private static GifImageDirectory readImageBlock(SequentialReader reader) throws IOException
+    {
+        GifImageDirectory imageDirectory = new GifImageDirectory();
+
+        imageDirectory.setInt(GifImageDirectory.TAG_LEFT, reader.getUInt16());
+        imageDirectory.setInt(GifImageDirectory.TAG_TOP, reader.getUInt16());
+        imageDirectory.setInt(GifImageDirectory.TAG_WIDTH, reader.getUInt16());
+        imageDirectory.setInt(GifImageDirectory.TAG_HEIGHT, reader.getUInt16());
+
+        byte flags = reader.getByte();
+        boolean hasColorTable = (flags & 0x7) != 0;
+        boolean isInterlaced = (flags & 0x40) != 0;
+        boolean isColorTableSorted = (flags & 0x20) != 0;
+
+        imageDirectory.setBoolean(GifImageDirectory.TAG_HAS_LOCAL_COLOUR_TABLE, hasColorTable);
+        imageDirectory.setBoolean(GifImageDirectory.TAG_IS_INTERLACED, isInterlaced);
+
+        if (hasColorTable)
+        {
+            imageDirectory.setBoolean(GifImageDirectory.TAG_IS_COLOR_TABLE_SORTED, isColorTableSorted);
+
+            int bitsPerPixel = (flags & 0x7) + 1;
+            imageDirectory.setInt(GifImageDirectory.TAG_LOCAL_COLOUR_TABLE_BITS_PER_PIXEL, bitsPerPixel);
+
+            // skip color table
+            reader.skip(3 * (2 << (flags & 0x7)));
+        }
+
+        // skip "LZW Minimum Code Size" byte
+        reader.getByte();
+
+        return imageDirectory;
+    }
+
+    private static byte[] gatherBytes(SequentialReader reader) throws IOException
+    {
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        byte[] buffer = new byte[257];
+
+        while (true)
+        {
+            byte b = reader.getByte();
+            if (b == 0)
+                return bytes.toByteArray();
+
+            int bInt = b & 0xFF;
+
+            buffer[0] = b;
+            reader.getBytes(buffer, 1, bInt);
+            bytes.write(buffer, 0, bInt + 1);
+        }
+    }
+
+    private static byte[] gatherBytes(SequentialReader reader, int firstLength) throws IOException
+    {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+        int length = firstLength;
+
+        while (length > 0)
+        {
+            buffer.write(reader.getBytes(length), 0, length);
+
+            length = reader.getByte();
+        }
+
+        return buffer.toByteArray();
+    }
+
+    private static void skipBlocks(SequentialReader reader) throws IOException
+    {
+        while (true)
+        {
+            short length = reader.getUInt8();
+
+            if (length == 0)
+                return;
+
+            reader.skip(length);
         }
     }
 }
diff --git a/Source/com/drew/metadata/gif/package-info.java b/Source/com/drew/metadata/gif/package-info.java
new file mode 100644
index 0000000..b28eb9e
--- /dev/null
+++ b/Source/com/drew/metadata/gif/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of GIF file metadata.
+ *
+ * @since 2.7.0
+ */
+package com.drew.metadata.gif;
diff --git a/Source/com/drew/metadata/gif/package.html b/Source/com/drew/metadata/gif/package.html
deleted file mode 100644
index 5915c52..0000000
--- a/Source/com/drew/metadata/gif/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of GIF file metadata.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/icc/IccDescriptor.java b/Source/com/drew/metadata/icc/IccDescriptor.java
index fd9544a..13363a6 100644
--- a/Source/com/drew/metadata/icc/IccDescriptor.java
+++ b/Source/com/drew/metadata/icc/IccDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,11 +29,15 @@ import com.drew.metadata.TagDescriptor;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.text.DecimalFormat;
+
+import static com.drew.metadata.icc.IccDirectory.*;
 
 /**
  * @author Yuri Binev
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class IccDescriptor extends TagDescriptor<IccDirectory>
 {
     public IccDescriptor(@NotNull IccDirectory directory)
@@ -45,13 +49,13 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case IccDirectory.TAG_PROFILE_VERSION:
+            case TAG_PROFILE_VERSION:
                 return getProfileVersionDescription();
-            case IccDirectory.TAG_PROFILE_CLASS:
+            case TAG_PROFILE_CLASS:
                 return getProfileClassDescription();
-            case IccDirectory.TAG_PLATFORM:
+            case TAG_PLATFORM:
                 return getPlatformDescription();
-            case IccDirectory.TAG_RENDERING_INTENT:
+            case TAG_RENDERING_INTENT:
                 return getRenderingIntentDescription();
         }
 
@@ -104,10 +108,10 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
                             observerString = "Unknown";
                             break;
                         case 1:
-                            observerString = "1931 2°";
+                            observerString = "1931 2\u00B0";
                             break;
                         case 2:
-                            observerString = "1964 10°";
+                            observerString = "1964 10\u00B0";
                             break;
                         default:
                             observerString = String.format("Unknown %d", observerType);
@@ -159,11 +163,13 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
                             illuminantString = String.format("Unknown %d", illuminantType);
                             break;
                     }
+                    DecimalFormat format = new DecimalFormat("0.###");
                     return String.format("%s Observer, Backing (%s, %s, %s), Geometry %s, Flare %d%%, Illuminant %s",
-                            observerString, x, y, z, geometryString, Math.round(flare * 100), illuminantString);
+                            observerString, format.format(x), format.format(y), format.format(z), geometryString, Math.round(flare * 100), illuminantString);
                 }
                 case ICC_TAG_TYPE_XYZ_ARRAY: {
                     StringBuilder res = new StringBuilder();
+                    DecimalFormat format = new DecimalFormat("0.####");
                     int count = (bytes.length - 8) / 12;
                     for (int i = 0; i < count; i++) {
                         float x = reader.getS15Fixed16(8 + i * 12);
@@ -171,7 +177,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
                         float z = reader.getS15Fixed16(8 + i * 12 + 8);
                         if (i > 0)
                             res.append(", ");
-                        res.append("(").append(x).append(", ").append(y).append(", ").append(z).append(")");
+                        res.append("(").append(format.format(x)).append(", ").append(format.format(y)).append(", ").append(format.format(z)).append(")");
                     }
                     return res.toString();
                 }
@@ -208,7 +214,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
                     return res.toString();
                 }
                 default:
-                    return String.format("%s(0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length);
+                    return String.format("%s (0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length);
             }
         } catch (IOException e) {
             // TODO decode these values during IccReader.extract so we can report any errors at that time
@@ -242,29 +248,17 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
     @Nullable
     private String getRenderingIntentDescription()
     {
-        Integer value = _directory.getInteger(IccDirectory.TAG_RENDERING_INTENT);
-
-        if (value == null)
-            return null;
-
-        switch (value) {
-            case 0:
-                return "Perceptual";
-            case 1:
-                return "Media-Relative Colorimetric";
-            case 2:
-                return "Saturation";
-            case 3:
-                return "ICC-Absolute Colorimetric";
-            default:
-                return String.format("Unknown (%d)", value);
-        }
+        return getIndexedDescription(TAG_RENDERING_INTENT,
+            "Perceptual",
+            "Media-Relative Colorimetric",
+            "Saturation",
+            "ICC-Absolute Colorimetric");
     }
 
     @Nullable
     private String getPlatformDescription()
     {
-        String str = _directory.getString(IccDirectory.TAG_PLATFORM);
+        String str = _directory.getString(TAG_PLATFORM);
         if (str==null)
             return null;
         // Because Java doesn't allow switching on string values, create an integer from the first four chars
@@ -294,7 +288,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
     @Nullable
     private String getProfileClassDescription()
     {
-        String str = _directory.getString(IccDirectory.TAG_PROFILE_CLASS);
+        String str = _directory.getString(TAG_PROFILE_CLASS);
         if (str==null)
             return null;
         // Because Java doesn't allow switching on string values, create an integer from the first four chars
@@ -328,7 +322,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory>
     @Nullable
     private String getProfileVersionDescription()
     {
-        Integer value = _directory.getInteger(IccDirectory.TAG_PROFILE_VERSION);
+        Integer value = _directory.getInteger(TAG_PROFILE_VERSION);
 
         if (value == null)
             return null;
diff --git a/Source/com/drew/metadata/icc/IccDirectory.java b/Source/com/drew/metadata/icc/IccDirectory.java
index 06d2454..182140a 100644
--- a/Source/com/drew/metadata/icc/IccDirectory.java
+++ b/Source/com/drew/metadata/icc/IccDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import java.util.HashMap;
  * @author Yuri Binev
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class IccDirectory extends Directory
 {
     // These (smaller valued) tags have an integer value that's equal to their offset within the ICC data buffer.
diff --git a/Source/com/drew/metadata/icc/IccReader.java b/Source/com/drew/metadata/icc/IccReader.java
index 3d099a8..090c9b4 100644
--- a/Source/com/drew/metadata/icc/IccReader.java
+++ b/Source/com/drew/metadata/icc/IccReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -23,58 +23,89 @@ package com.drew.metadata.icc;
 import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
 import com.drew.imaging.jpeg.JpegSegmentType;
 import com.drew.lang.ByteArrayReader;
+import com.drew.lang.DateUtil;
 import com.drew.lang.RandomAccessReader;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.MetadataReader;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.TimeZone;
+import java.util.Collections;
 
 /**
  * Reads an ICC profile.
+ * <p>
+ * More information about ICC:
  * <ul>
  * <li>http://en.wikipedia.org/wiki/ICC_profile</li>
  * <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ICC_Profile.html</li>
+ * <li>https://developer.apple.com/library/mac/samplecode/ImageApp/Listings/ICC_h.html</li>
  * </ul>
  *
  * @author Yuri Binev
- * @author Drew Noakes
+ * @author Drew Noakes https://drewnoakes.com
  */
 public class IccReader implements JpegSegmentMetadataReader, MetadataReader
 {
+    public static final String JPEG_SEGMENT_PREAMBLE = "ICC_PROFILE";
+
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APP2);
+        return Collections.singletonList(JpegSegmentType.APP2);
     }
 
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        return segmentBytes.length > 10 && "ICC_PROFILE".equalsIgnoreCase(new String(segmentBytes, 0, 11));
+        final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
+
+        // ICC data can be spread across multiple JPEG segments.
+        // We concat them together in this buffer for later processing.
+        byte[] buffer = null;
+
+        for (byte[] segmentBytes : segments) {
+            // Skip any segments that do not contain the required preamble
+            if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength)))
+                continue;
+
+            // NOTE we ignore three bytes here -- are they useful for anything?
+
+            // Grow the buffer
+            if (buffer == null) {
+                buffer = new byte[segmentBytes.length - 14];
+                // skip the first 14 bytes
+                System.arraycopy(segmentBytes, 14, buffer, 0, segmentBytes.length - 14);
+            } else {
+                byte[] newBuffer = new byte[buffer.length + segmentBytes.length - 14];
+                System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+                System.arraycopy(segmentBytes, 14, newBuffer, buffer.length, segmentBytes.length - 14);
+                buffer = newBuffer;
+            }
+        }
+
+        if (buffer != null)
+            extract(new ByteArrayReader(buffer), metadata);
     }
 
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
     {
-        // skip the first 14 bytes
-        byte[] iccProfileBytes = new byte[segmentBytes.length - 14];
-        System.arraycopy(segmentBytes, 14, iccProfileBytes, 0, segmentBytes.length - 14);
-
-        extract(new ByteArrayReader(iccProfileBytes), metadata);
+        extract(reader, metadata, null);
     }
 
-    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, @Nullable Directory parentDirectory)
     {
-        // TODO review whether the 'tagPtr' values below really do require ICC processing to work with a RandomAccessReader
+        // TODO review whether the 'tagPtr' values below really do require RandomAccessReader or whether SequentialReader may be used instead
+
+        IccDirectory directory = new IccDirectory();
 
-        final IccDirectory directory = metadata.getOrCreateDirectory(IccDirectory.class);
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
 
         try {
-            directory.setInt(IccDirectory.TAG_PROFILE_BYTE_COUNT, reader.getInt32(IccDirectory.TAG_PROFILE_BYTE_COUNT));
+            int profileByteCount = reader.getInt32(IccDirectory.TAG_PROFILE_BYTE_COUNT);
+            directory.setInt(IccDirectory.TAG_PROFILE_BYTE_COUNT, profileByteCount);
 
             // For these tags, the int value of the tag is in fact it's offset within the buffer.
             set4ByteString(directory, IccDirectory.TAG_CMM_TYPE, reader);
@@ -122,6 +153,8 @@ public class IccReader implements JpegSegmentMetadataReader, MetadataReader
         } catch (IOException ex) {
             directory.addError("Exception reading ICC profile: " + ex.getMessage());
         }
+
+        metadata.addDirectory(directory);
     }
 
     private void set4ByteString(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
@@ -156,12 +189,17 @@ public class IccReader implements JpegSegmentMetadataReader, MetadataReader
         final int M = reader.getUInt16(tagType + 8);
         final int s = reader.getUInt16(tagType + 10);
 
-//        final Date value = new Date(Date.UTC(y - 1900, m - 1, d, h, M, s));
-        final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
-        calendar.set(y, m, d, h, M, s);
-        final Date value = calendar.getTime();
-
-        directory.setDate(tagType, value);
+        if (DateUtil.isValidDate(y, m - 1, d) && DateUtil.isValidTime(h, M, s))
+        {
+            String dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", y, m, d, h, M, s);
+            directory.setString(tagType, dateString);
+        }
+        else
+        {
+            directory.addError(String.format(
+                "ICC data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d",
+                y, m, d, h, M, s));
+        }
     }
 
     @NotNull
diff --git a/Source/com/drew/metadata/icc/package-info.java b/Source/com/drew/metadata/icc/package-info.java
new file mode 100644
index 0000000..9e347d5
--- /dev/null
+++ b/Source/com/drew/metadata/icc/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of ICC (International Color Consortium) profile metadata.
+ */
+package com.drew.metadata.icc;
diff --git a/Source/com/drew/metadata/icc/package.html b/Source/com/drew/metadata/icc/package.html
deleted file mode 100644
index df41803..0000000
--- a/Source/com/drew/metadata/icc/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of ICC (International Color Consortium) profile metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/ico/IcoDescriptor.java b/Source/com/drew/metadata/ico/IcoDescriptor.java
new file mode 100644
index 0000000..952abcc
--- /dev/null
+++ b/Source/com/drew/metadata/ico/IcoDescriptor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.ico;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.ico.IcoDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class IcoDescriptor extends TagDescriptor<IcoDirectory>
+{
+    public IcoDescriptor(@NotNull IcoDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_IMAGE_TYPE:
+                return getImageTypeDescription();
+            case TAG_IMAGE_WIDTH:
+                return getImageWidthDescription();
+            case TAG_IMAGE_HEIGHT:
+                return getImageHeightDescription();
+            case TAG_COLOUR_PALETTE_SIZE:
+                return getColourPaletteSizeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getImageTypeDescription()
+    {
+        return getIndexedDescription(TAG_IMAGE_TYPE, 1, "Icon", "Cursor");
+    }
+
+    @Nullable
+    public String getImageWidthDescription()
+    {
+        Integer width = _directory.getInteger(TAG_IMAGE_WIDTH);
+        if (width == null)
+            return null;
+        return (width == 0 ? 256 : width) + " pixels";
+    }
+
+    @Nullable
+    public String getImageHeightDescription()
+    {
+        Integer width = _directory.getInteger(TAG_IMAGE_HEIGHT);
+        if (width == null)
+            return null;
+        return (width == 0 ? 256 : width) + " pixels";
+    }
+
+    @Nullable
+    public String getColourPaletteSizeDescription()
+    {
+        Integer size = _directory.getInteger(TAG_COLOUR_PALETTE_SIZE);
+        if (size == null)
+            return null;
+        return size == 0 ? "No palette" : size + " colour" + (size == 1 ? "" : "s");
+    }
+}
diff --git a/Source/com/drew/metadata/ico/IcoDirectory.java b/Source/com/drew/metadata/ico/IcoDirectory.java
new file mode 100644
index 0000000..a4a5423
--- /dev/null
+++ b/Source/com/drew/metadata/ico/IcoDirectory.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.ico;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class IcoDirectory extends Directory
+{
+    public static final int TAG_IMAGE_TYPE = 1;
+
+    public static final int TAG_IMAGE_WIDTH = 2;
+    public static final int TAG_IMAGE_HEIGHT = 3;
+    public static final int TAG_COLOUR_PALETTE_SIZE = 4;
+    public static final int TAG_COLOUR_PLANES = 5;
+    public static final int TAG_CURSOR_HOTSPOT_X = 6;
+    public static final int TAG_BITS_PER_PIXEL = 7;
+    public static final int TAG_CURSOR_HOTSPOT_Y = 8;
+    public static final int TAG_IMAGE_SIZE_BYTES = 9;
+    public static final int TAG_IMAGE_OFFSET_BYTES = 10;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_IMAGE_TYPE, "Image Type");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_COLOUR_PALETTE_SIZE, "Colour Palette Size");
+        _tagNameMap.put(TAG_COLOUR_PLANES, "Colour Planes");
+        _tagNameMap.put(TAG_CURSOR_HOTSPOT_X, "Hotspot X");
+        _tagNameMap.put(TAG_BITS_PER_PIXEL, "Bits Per Pixel");
+        _tagNameMap.put(TAG_CURSOR_HOTSPOT_Y, "Hotspot Y");
+        _tagNameMap.put(TAG_IMAGE_SIZE_BYTES, "Image Size Bytes");
+        _tagNameMap.put(TAG_IMAGE_OFFSET_BYTES, "Image Offset Bytes");
+    }
+
+    public IcoDirectory()
+    {
+        this.setDescriptor(new IcoDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "ICO";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/ico/IcoReader.java b/Source/com/drew/metadata/ico/IcoReader.java
new file mode 100644
index 0000000..49df0f2
--- /dev/null
+++ b/Source/com/drew/metadata/ico/IcoReader.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.ico;
+
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+
+/**
+ * Reads ICO (Windows Icon) file metadata.
+ * <ul>
+ * <li>https://en.wikipedia.org/wiki/ICO_(file_format)</li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class IcoReader
+{
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        reader.setMotorolaByteOrder(false);
+
+        int type;
+        int imageCount;
+
+        // Read header (ICONDIR structure)
+        try {
+            int reserved = reader.getUInt16();
+
+            if (reserved != 0) {
+                IcoDirectory directory = new IcoDirectory();
+                directory.addError("Invalid header bytes");
+                metadata.addDirectory(directory);
+                return;
+            }
+
+            type = reader.getUInt16();
+
+            if (type != 1 && type != 2) {
+                IcoDirectory directory = new IcoDirectory();
+                directory.addError("Invalid type " + type + " -- expecting 1 or 2");
+                metadata.addDirectory(directory);
+                return;
+            }
+
+            imageCount = reader.getUInt16();
+
+            if (imageCount == 0) {
+                IcoDirectory directory = new IcoDirectory();
+                directory.addError("Image count cannot be zero");
+                metadata.addDirectory(directory);
+                return;
+            }
+
+        } catch (IOException ex) {
+            IcoDirectory directory = new IcoDirectory();
+            directory.addError("Exception reading ICO file metadata: " + ex.getMessage());
+            metadata.addDirectory(directory);
+            return;
+        }
+
+        // Read each embedded image
+        for (int imageIndex = 0; imageIndex < imageCount; imageIndex++) {
+            IcoDirectory directory = new IcoDirectory();
+            try {
+                directory.setInt(IcoDirectory.TAG_IMAGE_TYPE, type);
+
+                directory.setInt(IcoDirectory.TAG_IMAGE_WIDTH, reader.getUInt8());
+                directory.setInt(IcoDirectory.TAG_IMAGE_HEIGHT, reader.getUInt8());
+                directory.setInt(IcoDirectory.TAG_COLOUR_PALETTE_SIZE, reader.getUInt8());
+                // Ignore this byte (normally zero, though .NET's System.Drawing.Icon.Save method writes 255)
+                reader.getUInt8();
+                if (type == 1) {
+                    // Icon
+                    directory.setInt(IcoDirectory.TAG_COLOUR_PLANES, reader.getUInt16());
+                    directory.setInt(IcoDirectory.TAG_BITS_PER_PIXEL, reader.getUInt16());
+                } else {
+                    // Cursor
+                    directory.setInt(IcoDirectory.TAG_CURSOR_HOTSPOT_X, reader.getUInt16());
+                    directory.setInt(IcoDirectory.TAG_CURSOR_HOTSPOT_Y, reader.getUInt16());
+                }
+                directory.setLong(IcoDirectory.TAG_IMAGE_SIZE_BYTES, reader.getUInt32());
+                directory.setLong(IcoDirectory.TAG_IMAGE_OFFSET_BYTES, reader.getUInt32());
+            } catch (IOException ex) {
+                directory.addError("Exception reading ICO file metadata: " + ex.getMessage());
+            }
+            metadata.addDirectory(directory);
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/ico/package-info.java b/Source/com/drew/metadata/ico/package-info.java
new file mode 100644
index 0000000..9f1b6e9
--- /dev/null
+++ b/Source/com/drew/metadata/ico/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of ICO (Windows Icon) file metadata.
+ */
+package com.drew.metadata.ico;
diff --git a/Source/com/drew/metadata/iptc/IptcDescriptor.java b/Source/com/drew/metadata/iptc/IptcDescriptor.java
index 25189a8..468dda5 100644
--- a/Source/com/drew/metadata/iptc/IptcDescriptor.java
+++ b/Source/com/drew/metadata/iptc/IptcDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -25,6 +25,8 @@ import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.iptc.IptcDirectory.*;
+
 /**
  * Provides human-readable string representations of tag values stored in a {@link IptcDirectory}.
  * <p>
@@ -32,6 +34,7 @@ import com.drew.metadata.TagDescriptor;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class IptcDescriptor extends TagDescriptor<IptcDirectory>
 {
     public IptcDescriptor(@NotNull IptcDirectory directory)
@@ -44,19 +47,63 @@ public class IptcDescriptor extends TagDescriptor<IptcDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case IptcDirectory.TAG_FILE_FORMAT:
+            case TAG_DATE_CREATED:
+                return getDateCreatedDescription();
+            case TAG_DIGITAL_DATE_CREATED:
+                return getDigitalDateCreatedDescription();
+            case TAG_DATE_SENT:
+                return getDateSentDescription();
+            case TAG_EXPIRATION_DATE:
+                return getExpirationDateDescription();
+            case TAG_EXPIRATION_TIME:
+                return getExpirationTimeDescription();
+            case TAG_FILE_FORMAT:
                 return getFileFormatDescription();
-            case IptcDirectory.TAG_KEYWORDS:
+            case TAG_KEYWORDS:
                 return getKeywordsDescription();
+            case TAG_REFERENCE_DATE:
+                return getReferenceDateDescription();
+            case TAG_RELEASE_DATE:
+                return getReleaseDateDescription();
+            case TAG_RELEASE_TIME:
+                return getReleaseTimeDescription();
+            case TAG_TIME_CREATED:
+                return getTimeCreatedDescription();
+            case TAG_DIGITAL_TIME_CREATED:
+                return getDigitalTimeCreatedDescription();
+            case TAG_TIME_SENT:
+                return getTimeSentDescription();
             default:
                 return super.getDescription(tagType);
         }
     }
 
+    @Nullable
+    public String getDateDescription(int tagType)
+    {
+        String s = _directory.getString(tagType);
+        if (s == null)
+            return null;
+        if (s.length() == 8)
+            return s.substring(0, 4) + ':' + s.substring(4, 6) + ':' + s.substring(6);
+        return s;
+    }
+
+    @Nullable
+    public String getTimeDescription(int tagType)
+    {
+        String s = _directory.getString(tagType);
+        if (s == null)
+            return null;
+        if (s.length() == 6 || s.length() == 11)
+            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
+        return s;
+    }
+
     @Nullable
     public String getFileFormatDescription()
     {
-        Integer value = _directory.getInteger(IptcDirectory.TAG_FILE_FORMAT);
+        Integer value = _directory.getInteger(TAG_FILE_FORMAT);
         if (value == null)
             return null;
         switch (value) {
@@ -97,67 +144,91 @@ public class IptcDescriptor extends TagDescriptor<IptcDirectory>
     @Nullable
     public String getByLineDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_BY_LINE);
+        return _directory.getString(TAG_BY_LINE);
     }
 
     @Nullable
     public String getByLineTitleDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_BY_LINE_TITLE);
+        return _directory.getString(TAG_BY_LINE_TITLE);
     }
 
     @Nullable
     public String getCaptionDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CAPTION);
+        return _directory.getString(TAG_CAPTION);
     }
 
     @Nullable
     public String getCategoryDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CATEGORY);
+        return _directory.getString(TAG_CATEGORY);
     }
 
     @Nullable
     public String getCityDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CITY);
+        return _directory.getString(TAG_CITY);
     }
 
     @Nullable
     public String getCopyrightNoticeDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_COPYRIGHT_NOTICE);
+        return _directory.getString(TAG_COPYRIGHT_NOTICE);
     }
 
     @Nullable
     public String getCountryOrPrimaryLocationDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME);
+        return _directory.getString(TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME);
     }
 
     @Nullable
     public String getCreditDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CREDIT);
+        return _directory.getString(TAG_CREDIT);
     }
 
     @Nullable
     public String getDateCreatedDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_DATE_CREATED);
+        return getDateDescription(TAG_DATE_CREATED);
+    }
+
+    @Nullable
+    public String getDigitalDateCreatedDescription()
+    {
+        return getDateDescription(TAG_DIGITAL_DATE_CREATED);
+    }
+
+    @Nullable
+    public String getDateSentDescription()
+    {
+        return getDateDescription(TAG_DATE_SENT);
+    }
+
+    @Nullable
+    public String getExpirationDateDescription()
+    {
+        return getDateDescription(TAG_EXPIRATION_DATE);
+    }
+
+    @Nullable
+    public String getExpirationTimeDescription()
+    {
+        return getTimeDescription(TAG_EXPIRATION_TIME);
     }
 
     @Nullable
     public String getHeadlineDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_HEADLINE);
+        return _directory.getString(TAG_HEADLINE);
     }
 
     @Nullable
     public String getKeywordsDescription()
     {
-        final String[] keywords = _directory.getStringArray(IptcDirectory.TAG_KEYWORDS);
+        final String[] keywords = _directory.getStringArray(TAG_KEYWORDS);
         if (keywords==null)
             return null;
         return StringUtil.join(keywords, ";");
@@ -166,78 +237,96 @@ public class IptcDescriptor extends TagDescriptor<IptcDirectory>
     @Nullable
     public String getObjectNameDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_OBJECT_NAME);
+        return _directory.getString(TAG_OBJECT_NAME);
     }
 
     @Nullable
     public String getOriginalTransmissionReferenceDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE);
+        return _directory.getString(TAG_ORIGINAL_TRANSMISSION_REFERENCE);
     }
 
     @Nullable
     public String getOriginatingProgramDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_ORIGINATING_PROGRAM);
+        return _directory.getString(TAG_ORIGINATING_PROGRAM);
     }
 
     @Nullable
     public String getProvinceOrStateDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_PROVINCE_OR_STATE);
+        return _directory.getString(TAG_PROVINCE_OR_STATE);
     }
 
     @Nullable
     public String getRecordVersionDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_APPLICATION_RECORD_VERSION);
+        return _directory.getString(TAG_APPLICATION_RECORD_VERSION);
+    }
+
+    @Nullable
+    public String getReferenceDateDescription()
+    {
+        return getDateDescription(TAG_REFERENCE_DATE);
     }
 
     @Nullable
     public String getReleaseDateDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_RELEASE_DATE);
+        return getDateDescription(TAG_RELEASE_DATE);
     }
 
     @Nullable
     public String getReleaseTimeDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_RELEASE_TIME);
+        return getTimeDescription(TAG_RELEASE_TIME);
     }
 
     @Nullable
     public String getSourceDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_SOURCE);
+        return _directory.getString(TAG_SOURCE);
     }
 
     @Nullable
     public String getSpecialInstructionsDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS);
+        return _directory.getString(TAG_SPECIAL_INSTRUCTIONS);
     }
 
     @Nullable
     public String getSupplementalCategoriesDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES);
+        return _directory.getString(TAG_SUPPLEMENTAL_CATEGORIES);
     }
 
     @Nullable
     public String getTimeCreatedDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_TIME_CREATED);
+        return getTimeDescription(TAG_TIME_CREATED);
+    }
+
+    @Nullable
+    public String getDigitalTimeCreatedDescription()
+    {
+        return getTimeDescription(TAG_DIGITAL_TIME_CREATED);
+    }
+
+    @Nullable
+    public String getTimeSentDescription()
+    {
+        return getTimeDescription(TAG_TIME_SENT);
     }
 
     @Nullable
     public String getUrgencyDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_URGENCY);
+        return _directory.getString(TAG_URGENCY);
     }
 
     @Nullable
     public String getWriterDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_CAPTION_WRITER);
+        return _directory.getString(TAG_CAPTION_WRITER);
     }
 }
diff --git a/Source/com/drew/metadata/iptc/IptcDirectory.java b/Source/com/drew/metadata/iptc/IptcDirectory.java
index b864a9b..0e436df 100644
--- a/Source/com/drew/metadata/iptc/IptcDirectory.java
+++ b/Source/com/drew/metadata/iptc/IptcDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,7 +24,11 @@ import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 
@@ -33,6 +37,7 @@ import java.util.List;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class IptcDirectory extends Directory
 {
     // IPTC EnvelopeRecord Tags
@@ -230,9 +235,84 @@ public class IptcDirectory extends Directory
     @Nullable
     public List<String> getKeywords()
     {
-        final String[] array = getStringArray(IptcDirectory.TAG_KEYWORDS);
+        final String[] array = getStringArray(TAG_KEYWORDS);
         if (array==null)
             return null;
         return Arrays.asList(array);
     }
+
+    /**
+     * Parses the Date Sent tag and the Time Sent tag to obtain a single Date object representing the
+     * date and time when the service sent this image.
+     * @return A Date object representing when the service sent this image, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateSent()
+    {
+        return getDate(TAG_DATE_SENT, TAG_TIME_SENT);
+    }
+
+    /**
+     * Parses the Release Date tag and the Release Time tag to obtain a single Date object representing the
+     * date and time when this image was released.
+     * @return A Date object representing when this image was released, if possible, otherwise null
+     */
+    @Nullable
+    public Date getReleaseDate()
+    {
+        return getDate(TAG_RELEASE_DATE, TAG_RELEASE_TIME);
+    }
+
+    /**
+     * Parses the Expiration Date tag and the Expiration Time tag to obtain a single Date object representing
+     * that this image should not used after this date and time.
+     * @return A Date object representing when this image was released, if possible, otherwise null
+     */
+    @Nullable
+    public Date getExpirationDate()
+    {
+        return getDate(TAG_EXPIRATION_DATE, TAG_EXPIRATION_TIME);
+    }
+
+    /**
+     * Parses the Date Created tag and the Time Created tag to obtain a single Date object representing the
+     * date and time when this image was captured.
+     * @return A Date object representing when this image was captured, if possible, otherwise null
+     */
+    @Nullable
+    public Date getDateCreated()
+    {
+        return getDate(TAG_DATE_CREATED, TAG_TIME_CREATED);
+    }
+
+    /**
+     * Parses the Digital Date Created tag and the Digital Time Created tag to obtain a single Date object
+     * representing the date and time when the digital representation of this image was created.
+     * @return A Date object representing when the digital representation of this image was created,
+     * if possible, otherwise null
+     */
+    @Nullable
+    public Date getDigitalDateCreated()
+    {
+        return getDate(TAG_DIGITAL_DATE_CREATED, TAG_DIGITAL_TIME_CREATED);
+    }
+
+    @Nullable
+    private Date getDate(int dateTagType, int timeTagType)
+    {
+        String date = getString(dateTagType);
+        String time = getString(timeTagType);
+
+        if (date == null)
+            return null;
+        if (time == null)
+            return null;
+
+        try {
+            DateFormat parser = new SimpleDateFormat("yyyyMMddHHmmssZ");
+            return parser.parse(date + time);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
 }
diff --git a/Source/com/drew/metadata/iptc/IptcReader.java b/Source/com/drew/metadata/iptc/IptcReader.java
index 1745f1a..3787bc7 100644
--- a/Source/com/drew/metadata/iptc/IptcReader.java
+++ b/Source/com/drew/metadata/iptc/IptcReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -25,12 +25,14 @@ 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.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Date;
+import java.nio.charset.Charset;
+import java.util.Collections;
 
 /**
  * Decodes IPTC binary data, populating a {@link Metadata} object with tag values in an {@link IptcDirectory}.
@@ -55,30 +57,42 @@ public class IptcReader implements JpegSegmentMetadataReader
     public static final int DATA_RECORD = 8;
     public static final int POST_DATA_RECORD = 9;
 */
+    private static final byte IptcMarkerByte = 0x1c;
 
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APPD);
+        return Collections.singletonList(JpegSegmentType.APPD);
     }
 
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        // Check whether the first byte resembles
-        return segmentBytes.length != 0 && segmentBytes[0] == 0x1c;
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the IPTC marker byte
+            if (segmentBytes.length != 0 && segmentBytes[0] == IptcMarkerByte) {
+                extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
+            }
+        }
     }
 
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    /**
+     * 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)
     {
-        extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
+        extract(reader, metadata, length, null);
     }
 
     /**
      * Performs the IPTC data extraction, adding found values to the specified instance of {@link Metadata}.
      */
-    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length)
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length, @Nullable Directory parentDirectory)
     {
-        IptcDirectory directory = metadata.getOrCreateDirectory(IptcDirectory.class);
+        IptcDirectory directory = new IptcDirectory();
+        metadata.addDirectory(directory);
+
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
 
         int offset = 0;
 
@@ -95,16 +109,16 @@ public class IptcReader implements JpegSegmentMetadataReader
                 return;
             }
 
-            if (startByte != 0x1c) {
+            if (startByte != IptcMarkerByte) {
                 // NOTE have seen images where there was one extra byte at the end, giving
                 // offset==length at this point, which is not worth logging as an error.
                 if (offset != length)
-                    directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x1c' but got '0x" + Integer.toHexString(startByte) + "'.");
+                    directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x" + Integer.toHexString(IptcMarkerByte) + "' but got '0x" + Integer.toHexString(startByte) + "'.");
                 return;
             }
 
             // we need at least five bytes left to read a tag
-            if (offset + 5 >= length) {
+            if (offset + 5 > length) {
                 directory.addError("Too few bytes remain for a valid IPTC tag");
                 return;
             }
@@ -152,18 +166,15 @@ public class IptcReader implements JpegSegmentMetadataReader
             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) {
+                String charsetName = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes);
+                if (charsetName == null) {
                     // Unable to determine the charset, so fall through and treat tag as a regular string
-                    string = new String(bytes);
-                    break;
+                    charsetName = new String(bytes);
                 }
-                directory.setString(tagIdentifier, charset);
+                directory.setString(tagIdentifier, charsetName);
                 return;
             case IptcDirectory.TAG_ENVELOPE_RECORD_VERSION:
             case IptcDirectory.TAG_APPLICATION_RECORD_VERSION:
@@ -183,58 +194,44 @@ public class IptcReader implements JpegSegmentMetadataReader
                 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);
-            }
+        String charSetName = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET);
+        Charset charset = null;
+        try {
+            if (charSetName != null)
+                charset = Charset.forName(charSetName);
+        } catch (Throwable ignored) {
+        }
+
+        StringValue string;
+        if (charSetName != null) {
+            string = reader.getStringValue(tagByteCount, charset);
+        } else {
+            byte[] bytes = reader.getBytes(tagByteCount);
+            Charset charSet = Iso2022Converter.guessCharSet(bytes);
+            string = charSet != null ? new StringValue(bytes, charSet) : new StringValue(bytes, null);
         }
 
         if (directory.containsTag(tagIdentifier)) {
-            // this fancy string[] business avoids using an ArrayList for performance reasons
-            String[] oldStrings = directory.getStringArray(tagIdentifier);
-            String[] newStrings;
+            // this fancy StringValue[] business avoids using an ArrayList for performance reasons
+            StringValue[] oldStrings = directory.getStringValueArray(tagIdentifier);
+            StringValue[] newStrings;
             if (oldStrings == null) {
-                newStrings = new String[1];
+                // TODO hitting this block means any prior value(s) are discarded
+                newStrings = new StringValue[1];
             } else {
-                newStrings = new String[oldStrings.length + 1];
+                newStrings = new StringValue[oldStrings.length + 1];
                 System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length);
             }
             newStrings[newStrings.length - 1] = string;
-            directory.setStringArray(tagIdentifier, newStrings);
+            directory.setStringValueArray(tagIdentifier, newStrings);
         } else {
-            directory.setString(tagIdentifier, string);
+            directory.setStringValue(tagIdentifier, string);
         }
     }
 }
diff --git a/Source/com/drew/metadata/iptc/Iso2022Converter.java b/Source/com/drew/metadata/iptc/Iso2022Converter.java
index 5edd749..8beb9a1 100644
--- a/Source/com/drew/metadata/iptc/Iso2022Converter.java
+++ b/Source/com/drew/metadata/iptc/Iso2022Converter.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.iptc;
 
 import com.drew.lang.annotations.NotNull;
@@ -38,19 +58,19 @@ public final class Iso2022Converter
     }
 
     /**
-     * Attempts to guess the encoding of a string provided as a byte array.
-     * <p/>
-     * Encodings trialled are, in order:
+     * Attempts to guess the {@link Charset} of a string provided as a byte array.
+     * <p>
+     * Charsets 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
+     * <p>
+     * Its only purpose is to guess the Charset if and only if IPTC tag coded character set is not set. If the
      * encoding is not UTF-8, the tag should be set. Otherwise it is bad practice. This method tries to
      * workaround this issue since some metadata manipulating tools do not prevent such bad practice.
-     * <p/>
+     * <p>
      * About the reliability of this method: The check if some bytes are UTF-8 or not has a very high reliability.
      * The two other checks are less reliable.
      *
@@ -58,17 +78,18 @@ public final class Iso2022Converter
      * @return the name of the encoding or null if none could be guessed
      */
     @Nullable
-    static String guessEncoding(@NotNull final byte[] bytes)
+    static Charset guessCharSet(@NotNull final byte[] bytes)
     {
         String[] encodings = { UTF_8, System.getProperty("file.encoding"), ISO_8859_1 };
 
         for (String encoding : encodings)
         {
-            CharsetDecoder cs = Charset.forName(encoding).newDecoder();
+            Charset charset = Charset.forName(encoding);
+            CharsetDecoder cs = charset.newDecoder();
 
             try {
                 cs.decode(ByteBuffer.wrap(bytes));
-                return encoding;
+                return charset;
             } catch (CharacterCodingException e) {
                 // fall through...
             }
diff --git a/Source/com/drew/metadata/iptc/package-info.java b/Source/com/drew/metadata/iptc/package-info.java
new file mode 100644
index 0000000..8e0f4f9
--- /dev/null
+++ b/Source/com/drew/metadata/iptc/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of IPTC metadata.
+ */
+package com.drew.metadata.iptc;
diff --git a/Source/com/drew/metadata/iptc/package.html b/Source/com/drew/metadata/iptc/package.html
deleted file mode 100644
index 40c60b3..0000000
--- a/Source/com/drew/metadata/iptc/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/metadata/jfif/JfifDescriptor.java b/Source/com/drew/metadata/jfif/JfifDescriptor.java
index 25bf64d..d6f175e 100644
--- a/Source/com/drew/metadata/jfif/JfifDescriptor.java
+++ b/Source/com/drew/metadata/jfif/JfifDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,13 +24,19 @@ import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.jfif.JfifDirectory.*;
+
 /**
  * Provides human-readable string versions of the tags stored in a JfifDirectory.
- * <p>
- * More info at: http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
+ *
+ * <ul>
+ *   <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li>
+ *   <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li>
+ * </ul>
  *
  * @author Yuri Binev, Drew Noakes
  */
+@SuppressWarnings("WeakerAccess")
 public class JfifDescriptor extends TagDescriptor<JfifDirectory>
 {
     public JfifDescriptor(@NotNull JfifDirectory directory)
@@ -43,13 +49,13 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case JfifDirectory.TAG_RESX:
+            case TAG_RESX:
                 return getImageResXDescription();
-            case JfifDirectory.TAG_RESY:
+            case TAG_RESY:
                 return getImageResYDescription();
-            case JfifDirectory.TAG_VERSION:
+            case TAG_VERSION:
                 return getImageVersionDescription();
-            case JfifDirectory.TAG_UNITS:
+            case TAG_UNITS:
                 return getImageResUnitsDescription();
             default:
                 return super.getDescription(tagType);
@@ -59,7 +65,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory>
     @Nullable
     public String getImageVersionDescription()
     {
-        Integer value = _directory.getInteger(JfifDirectory.TAG_VERSION);
+        Integer value = _directory.getInteger(TAG_VERSION);
         if (value==null)
             return null;
         return String.format("%d.%d", (value & 0xFF00) >> 8, value & 0xFF);
@@ -68,7 +74,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory>
     @Nullable
     public String getImageResYDescription()
     {
-        Integer value = _directory.getInteger(JfifDirectory.TAG_RESY);
+        Integer value = _directory.getInteger(TAG_RESY);
         if (value==null)
             return null;
         return String.format("%d dot%s",
@@ -79,7 +85,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory>
     @Nullable
     public String getImageResXDescription()
     {
-        Integer value = _directory.getInteger(JfifDirectory.TAG_RESX);
+        Integer value = _directory.getInteger(TAG_RESX);
         if (value==null)
             return null;
         return String.format("%d dot%s",
@@ -90,7 +96,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory>
     @Nullable
     public String getImageResUnitsDescription()
     {
-        Integer value = _directory.getInteger(JfifDirectory.TAG_UNITS);
+        Integer value = _directory.getInteger(TAG_UNITS);
         if (value==null)
             return null;
         switch (value) {
diff --git a/Source/com/drew/metadata/jfif/JfifDirectory.java b/Source/com/drew/metadata/jfif/JfifDirectory.java
index 947153d..b9e477d 100644
--- a/Source/com/drew/metadata/jfif/JfifDirectory.java
+++ b/Source/com/drew/metadata/jfif/JfifDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import java.util.HashMap;
  *
  * @author Yuri Binev, Drew Noakes
  */
+@SuppressWarnings("WeakerAccess")
 public class JfifDirectory extends Directory
 {
     public static final int TAG_VERSION = 5;
@@ -38,6 +39,8 @@ public class JfifDirectory extends Directory
     public static final int TAG_UNITS = 7;
     public static final int TAG_RESX = 8;
     public static final int TAG_RESY = 10;
+    public static final int TAG_THUMB_WIDTH = 12;
+    public static final int TAG_THUMB_HEIGHT = 13;
 
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
@@ -48,6 +51,8 @@ public class JfifDirectory extends Directory
         _tagNameMap.put(TAG_UNITS, "Resolution Units");
         _tagNameMap.put(TAG_RESY, "Y Resolution");
         _tagNameMap.put(TAG_RESX, "X Resolution");
+        _tagNameMap.put(TAG_THUMB_WIDTH, "Thumbnail Width Pixels");
+        _tagNameMap.put(TAG_THUMB_HEIGHT, "Thumbnail Height Pixels");
     }
 
     public JfifDirectory()
@@ -79,14 +84,31 @@ public class JfifDirectory extends Directory
         return getInt(JfifDirectory.TAG_UNITS);
     }
 
+    /**
+     * @deprecated use {@link #getResY} instead.
+     */
+    @Deprecated
     public int getImageWidth() throws MetadataException
     {
         return getInt(JfifDirectory.TAG_RESY);
     }
 
+    public int getResY() throws MetadataException
+    {
+        return getInt(JfifDirectory.TAG_RESY);
+    }
+
+    /**
+     * @deprecated use {@link #getResX} instead.
+     */
+    @Deprecated
     public int getImageHeight() throws MetadataException
     {
         return getInt(JfifDirectory.TAG_RESX);
     }
 
+    public int getResX() throws MetadataException
+    {
+        return getInt(JfifDirectory.TAG_RESX);
+    }
 }
diff --git a/Source/com/drew/metadata/jfif/JfifReader.java b/Source/com/drew/metadata/jfif/JfifReader.java
index f29fd90..55db301 100644
--- a/Source/com/drew/metadata/jfif/JfifReader.java
+++ b/Source/com/drew/metadata/jfif/JfifReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,31 +29,35 @@ import com.drew.metadata.Metadata;
 import com.drew.metadata.MetadataReader;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * Reader for JFIF data, found in the APP0 JPEG segment.
- * <p>
- * More info at: http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
+ *
+ * <ul>
+ *   <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li>
+ *   <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li>
+ * </ul>
  *
  * @author Yuri Binev, Drew Noakes, Markus Meyer
  */
 public class JfifReader implements JpegSegmentMetadataReader, MetadataReader
 {
+    public static final String PREAMBLE = "JFIF";
+
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APP0);
-    }
-
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
-    {
-        return segmentBytes.length > 3 && "JFIF".equals(new String(segmentBytes, 0, 4));
+        return Collections.singletonList(JpegSegmentType.APP0);
     }
 
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        extract(new ByteArrayReader(segmentBytes), metadata);
+        for (byte[] segmentBytes : segments) {
+            // Skip segments not starting with the required header
+            if (segmentBytes.length >= PREAMBLE.length() && PREAMBLE.equals(new String(segmentBytes, 0, PREAMBLE.length())))
+                extract(new ByteArrayReader(segmentBytes), metadata);
+        }
     }
 
     /**
@@ -62,23 +66,18 @@ public class JfifReader implements JpegSegmentMetadataReader, MetadataReader
      */
     public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
     {
-        JfifDirectory directory = metadata.getOrCreateDirectory(JfifDirectory.class);
+        JfifDirectory directory = new JfifDirectory();
+        metadata.addDirectory(directory);
 
         try {
             // For JFIF, the tag number is also the offset into the segment
 
-            int ver = reader.getUInt16(JfifDirectory.TAG_VERSION);
-            directory.setInt(JfifDirectory.TAG_VERSION, ver);
-
-            int units = reader.getUInt8(JfifDirectory.TAG_UNITS);
-            directory.setInt(JfifDirectory.TAG_UNITS, units);
-
-            int height = reader.getUInt16(JfifDirectory.TAG_RESX);
-            directory.setInt(JfifDirectory.TAG_RESX, height);
-
-            int width = reader.getUInt16(JfifDirectory.TAG_RESY);
-            directory.setInt(JfifDirectory.TAG_RESY, width);
-
+            directory.setInt(JfifDirectory.TAG_VERSION,      reader.getUInt16(JfifDirectory.TAG_VERSION));
+            directory.setInt(JfifDirectory.TAG_UNITS,        reader.getUInt8(JfifDirectory.TAG_UNITS));
+            directory.setInt(JfifDirectory.TAG_RESX,         reader.getUInt16(JfifDirectory.TAG_RESX));
+            directory.setInt(JfifDirectory.TAG_RESY,         reader.getUInt16(JfifDirectory.TAG_RESY));
+            directory.setInt(JfifDirectory.TAG_THUMB_WIDTH,  reader.getUInt8(JfifDirectory.TAG_THUMB_WIDTH));
+            directory.setInt(JfifDirectory.TAG_THUMB_HEIGHT, reader.getUInt8(JfifDirectory.TAG_THUMB_HEIGHT));
         } catch (IOException me) {
             directory.addError(me.getMessage());
         }
diff --git a/Source/com/drew/metadata/jfif/package-info.java b/Source/com/drew/metadata/jfif/package-info.java
new file mode 100644
index 0000000..1c65e5b
--- /dev/null
+++ b/Source/com/drew/metadata/jfif/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of JFIF metadata.
+ */
+package com.drew.metadata.jfif;
diff --git a/Source/com/drew/metadata/jfif/package.html b/Source/com/drew/metadata/jfif/package.html
deleted file mode 100644
index 384a7d9..0000000
--- a/Source/com/drew/metadata/jfif/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of JFIF metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/jfxx/JfxxDescriptor.java b/Source/com/drew/metadata/jfxx/JfxxDescriptor.java
new file mode 100644
index 0000000..4e0ce98
--- /dev/null
+++ b/Source/com/drew/metadata/jfxx/JfxxDescriptor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jfxx;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.jfxx.JfxxDirectory.*;
+
+/**
+ * Provides human-readable string versions of the tags stored in a JfxxDirectory.
+ *
+ * <ul>
+ *   <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li>
+ *   <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li>
+ * </ul>
+ *
+ * @author Drew Noakes
+ */
+@SuppressWarnings("WeakerAccess")
+public class JfxxDescriptor extends TagDescriptor<JfxxDirectory>
+{
+    public JfxxDescriptor(@NotNull JfxxDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_EXTENSION_CODE:
+                return getExtensionCodeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getExtensionCodeDescription()
+    {
+        Integer value = _directory.getInteger(TAG_EXTENSION_CODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0x10: return "Thumbnail coded using JPEG";
+            case 0x11: return "Thumbnail stored using 1 byte/pixel";
+            case 0x13: return "Thumbnail stored using 3 bytes/pixel";
+            default: return "Unknown extension code " + value;
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/jfxx/JfxxDirectory.java b/Source/com/drew/metadata/jfxx/JfxxDirectory.java
new file mode 100644
index 0000000..dd101da
--- /dev/null
+++ b/Source/com/drew/metadata/jfxx/JfxxDirectory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jfxx;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.MetadataException;
+
+import java.util.HashMap;
+
+/**
+ * Directory of tags and values for the SOF0 JFXX segment.
+ *
+ * @author Drew Noakes
+ */
+@SuppressWarnings("WeakerAccess")
+public class JfxxDirectory extends Directory
+{
+    public static final int TAG_EXTENSION_CODE = 5;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_EXTENSION_CODE, "Extension Code");
+    }
+
+    public JfxxDirectory()
+    {
+        this.setDescriptor(new JfxxDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "JFXX";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    public int getExtensionCode() throws MetadataException
+    {
+        return getInt(JfxxDirectory.TAG_EXTENSION_CODE);
+    }
+}
diff --git a/Source/com/drew/metadata/jfxx/JfxxReader.java b/Source/com/drew/metadata/jfxx/JfxxReader.java
new file mode 100644
index 0000000..ca68ad9
--- /dev/null
+++ b/Source/com/drew/metadata/jfxx/JfxxReader.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jfxx;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.MetadataReader;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Reader for JFXX (JFIF extensions) data, found in the APP0 JPEG segment.
+ *
+ * <ul>
+ *   <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li>
+ *   <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li>
+ * </ul>
+ *
+ * @author Drew Noakes
+ */
+public class JfxxReader implements JpegSegmentMetadataReader, MetadataReader
+{
+    public static final String PREAMBLE = "JFXX";
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.APP0);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            // Skip segments not starting with the required header
+            if (segmentBytes.length >= PREAMBLE.length() && PREAMBLE.equals(new String(segmentBytes, 0, PREAMBLE.length())))
+                extract(new ByteArrayReader(segmentBytes), metadata);
+        }
+    }
+
+    /**
+     * Performs the JFXX data extraction, adding found values to the specified
+     * instance of {@link Metadata}.
+     */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
+    {
+        JfxxDirectory directory = new JfxxDirectory();
+        metadata.addDirectory(directory);
+
+        try {
+            // For JFXX, the tag number is also the offset into the segment
+
+            directory.setInt(JfxxDirectory.TAG_EXTENSION_CODE, reader.getUInt8(JfxxDirectory.TAG_EXTENSION_CODE));
+        } catch (IOException me) {
+            directory.addError(me.getMessage());
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/jfxx/package-info.java b/Source/com/drew/metadata/jfxx/package-info.java
new file mode 100644
index 0000000..8396977
--- /dev/null
+++ b/Source/com/drew/metadata/jfxx/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of JFXX (JFIF extension) metadata.
+ */
+package com.drew.metadata.jfxx;
diff --git a/Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java b/Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java
new file mode 100644
index 0000000..cb83321
--- /dev/null
+++ b/Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.jpeg.HuffmanTablesDirectory.*;
+
+/**
+ * Provides a human-readable string version of the tag stored in a HuffmanTableDirectory.
+ *
+ * <ul>
+ *   <li>https://en.wikipedia.org/wiki/Huffman_coding</li>
+ *   <li>http://stackoverflow.com/a/4954117</li>
+ * </ul>
+ *
+ * @author Nadahar
+ */
+@SuppressWarnings("WeakerAccess")
+public class HuffmanTablesDescriptor extends TagDescriptor<HuffmanTablesDirectory>
+{
+    public HuffmanTablesDescriptor(@NotNull HuffmanTablesDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_NUMBER_OF_TABLES:
+                return getNumberOfTablesDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getNumberOfTablesDescription()
+    {
+        Integer value = _directory.getInteger(TAG_NUMBER_OF_TABLES);
+        if (value==null)
+            return null;
+        return value + (value == 1 ? " Huffman table" : " Huffman tables");
+    }
+}
diff --git a/Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java b/Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java
new file mode 100644
index 0000000..b03a518
--- /dev/null
+++ b/Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.MetadataException;
+
+/**
+ * Directory of tables for the DHT (Define Huffman Table(s)) segment.
+ *
+ * @author Nadahar
+ */
+@SuppressWarnings("WeakerAccess")
+public class HuffmanTablesDirectory extends Directory {
+
+    public static final int TAG_NUMBER_OF_TABLES = 1;
+
+    protected static final byte[] TYPICAL_LUMINANCE_DC_LENGTHS = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01,
+        (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
+    };
+
+    protected static final byte[] TYPICAL_LUMINANCE_DC_VALUES = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
+        (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_DC_LENGTHS = {
+        (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01,
+        (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_DC_VALUES = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07,
+        (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B
+    };
+
+    protected static final byte[] TYPICAL_LUMINANCE_AC_LENGTHS = {
+        (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x03, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x03,
+        (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x7D
+    };
+
+    protected static final byte[] TYPICAL_LUMINANCE_AC_VALUES = {
+        (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x11, (byte) 0x05, (byte) 0x12,
+        (byte) 0x21, (byte) 0x31, (byte) 0x41, (byte) 0x06, (byte) 0x13, (byte) 0x51, (byte) 0x61, (byte) 0x07,
+        (byte) 0x22, (byte) 0x71, (byte) 0x14, (byte) 0x32, (byte) 0x81, (byte) 0x91, (byte) 0xA1, (byte) 0x08,
+        (byte) 0x23, (byte) 0x42, (byte) 0xB1, (byte) 0xC1, (byte) 0x15, (byte) 0x52, (byte) 0xD1, (byte) 0xF0,
+        (byte) 0x24, (byte) 0x33, (byte) 0x62, (byte) 0x72, (byte) 0x82, (byte) 0x09, (byte) 0x0A, (byte) 0x16,
+        (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28,
+        (byte) 0x29, (byte) 0x2A, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39,
+        (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49,
+        (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59,
+        (byte) 0x5A, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69,
+        (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79,
+        (byte) 0x7A, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89,
+        (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98,
+        (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7,
+        (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6,
+        (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5,
+        (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4,
+        (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE1, (byte) 0xE2,
+        (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA,
+        (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8,
+        (byte) 0xF9, (byte) 0xFA
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_AC_LENGTHS = {
+        (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x03, (byte) 0x04,
+        (byte) 0x07, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x77
+    };
+
+    protected static final byte[] TYPICAL_CHROMINANCE_AC_VALUES = {
+        (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x11, (byte) 0x04, (byte) 0x05, (byte) 0x21,
+        (byte) 0x31, (byte) 0x06, (byte) 0x12, (byte) 0x41, (byte) 0x51, (byte) 0x07, (byte) 0x61, (byte) 0x71,
+        (byte) 0x13, (byte) 0x22, (byte) 0x32, (byte) 0x81, (byte) 0x08, (byte) 0x14, (byte) 0x42, (byte) 0x91,
+        (byte) 0xA1, (byte) 0xB1, (byte) 0xC1, (byte) 0x09, (byte) 0x23, (byte) 0x33, (byte) 0x52, (byte) 0xF0,
+        (byte) 0x15, (byte) 0x62, (byte) 0x72, (byte) 0xD1, (byte) 0x0A, (byte) 0x16, (byte) 0x24, (byte) 0x34,
+        (byte) 0xE1, (byte) 0x25, (byte) 0xF1, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x26,
+        (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38,
+        (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48,
+        (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58,
+        (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68,
+        (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78,
+        (byte) 0x79, (byte) 0x7A, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87,
+        (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96,
+        (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5,
+        (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4,
+        (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3,
+        (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2,
+        (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA,
+        (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9,
+        (byte) 0xEA, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8,
+        (byte) 0xF9, (byte) 0xFA
+    };
+
+    @NotNull
+    protected final List<HuffmanTable> tables = new ArrayList<HuffmanTable>(4);
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_NUMBER_OF_TABLES, "Number of Tables");
+    }
+
+    public HuffmanTablesDirectory()
+    {
+        this.setDescriptor(new HuffmanTablesDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Huffman";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * @param tableNumber The zero-based index of the table. This number is normally between 0 and 3.
+     *                    Use {@link #getNumberOfTables} for bounds-checking.
+     * @return The {@link HuffmanTable} having the specified number.
+     */
+    @NotNull
+    public HuffmanTable getTable(int tableNumber)
+    {
+        return tables.get(tableNumber);
+    }
+
+    /**
+     * @return The number of Huffman tables held by this {@link HuffmanTablesDirectory} instance.
+     */
+    public int getNumberOfTables() throws MetadataException
+    {
+        return getInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES);
+    }
+
+    /**
+     * @return The {@link List} of {@link HuffmanTable}s in this
+     *         {@link Directory}.
+     */
+    @NotNull
+    protected List<HuffmanTable> getTables() {
+        return tables;
+    }
+
+    /**
+     * Evaluates whether all the tables in this {@link HuffmanTablesDirectory}
+     * are "typical" Huffman tables.
+     * <p>
+     * "Typical" has a special meaning in this context as the JPEG standard
+     * (ISO/IEC 10918 or ITU-T T.81) defines 4 Huffman tables that has been
+     * developed from the average statistics of a large set of images with 8-bit
+     * precision. Using these instead of calculating the optimal Huffman tables
+     * for a given image is faster, and is preferred by many hardware encoders
+     * and some hardware decoders.
+     * <p>
+     * Even though the JPEG standard doesn't define these as "standard tables"
+     * and requires a decoder to be able to read any valid Huffman tables, some
+     * are in reality limited decoding images using these "typical" tables.
+     * Standards like DCF (Design rule for Camera File system) and DLNA (Digital
+     * Living Network Alliance) actually requires any compliant JPEG to use only
+     * the "typical" Huffman tables.
+     * <p>
+     * This is also related to the term "optimized" JPEG. An "optimized" JPEG is
+     * a JPEG that doesn't use the "typical" Huffman tables.
+     *
+     * @return Whether or not all the tables in this
+     *         {@link HuffmanTablesDirectory} are the predefined "typical"
+     *         Huffman tables.
+     */
+    public boolean isTypical() {
+        if (tables.size() == 0) {
+            return false;
+        }
+        for (HuffmanTable table : tables) {
+            if (!table.isTypical()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * The opposite of {@link #isTypical()}.
+     *
+     * @return Whether or not the tables in this {@link HuffmanTablesDirectory}
+     *         are "optimized" - which means that at least one of them aren't
+     *         one of the "typical" Huffman tables.
+     */
+    public boolean isOptimized() {
+        return !isTypical();
+    }
+
+    /**
+     * An instance of this class holds a JPEG Huffman table.
+     */
+    public static class HuffmanTable {
+        private final int tableLength;
+        private final HuffmanTableClass tableClass;
+        private final int tableDestinationId;
+        private final byte[] lengthBytes;
+        private final byte[] valueBytes;
+
+        public HuffmanTable (
+            @NotNull HuffmanTableClass
+            tableClass,
+            int tableDestinationId,
+            @NotNull byte[] lBytes,
+            @NotNull byte[] vBytes
+        ) {
+            this.tableClass = tableClass;
+            this.tableDestinationId = tableDestinationId;
+            this.lengthBytes = lBytes;
+            this.valueBytes = vBytes;
+            this.tableLength = vBytes.length + 17;
+        }
+
+        /**
+         * @return The table length in bytes.
+         */
+        public int getTableLength() {
+            return tableLength;
+        }
+
+
+        /**
+         * @return The {@link HuffmanTableClass} of this table.
+         */
+        public HuffmanTableClass getTableClass() {
+            return tableClass;
+        }
+
+
+        /**
+         * @return the the destination identifier for this table.
+         */
+        public int getTableDestinationId() {
+            return tableDestinationId;
+        }
+
+
+        /**
+         * @return A byte array with the L values for this table.
+         */
+        public byte[] getLengthBytes() {
+            if (lengthBytes == null)
+                return null;
+            byte[] result = new byte[lengthBytes.length];
+            System.arraycopy(lengthBytes, 0, result, 0, lengthBytes.length);
+            return result;
+        }
+
+
+        /**
+         * @return A byte array with the V values for this table.
+         */
+        public byte[] getValueBytes() {
+            if (valueBytes == null)
+                return null;
+            byte[] result = new byte[valueBytes.length];
+            System.arraycopy(valueBytes, 0, result, 0, valueBytes.length);
+            return result;
+        }
+
+        /**
+         * Evaluates whether this table is a "typical" Huffman table.
+         * <p>
+         * "Typical" has a special meaning in this context as the JPEG standard
+         * (ISO/IEC 10918 or ITU-T T.81) defines 4 Huffman tables that has been
+         * developed from the average statistics of a large set of images with
+         * 8-bit precision. Using these instead of calculating the optimal
+         * Huffman tables for a given image is faster, and is preferred by many
+         * hardware encoders and some hardware decoders.
+         * <p>
+         * Even though the JPEG standard doesn't define these as
+         * "standard tables" and requires a decoder to be able to read any valid
+         * Huffman tables, some are in reality limited decoding images using
+         * these "typical" tables. Standards like DCF (Design rule for Camera
+         * File system) and DLNA (Digital Living Network Alliance) actually
+         * requires any compliant JPEG to use only the "typical" Huffman tables.
+         * <p>
+         * This is also related to the term "optimized" JPEG. An "optimized"
+         * JPEG is a JPEG that doesn't use the "typical" Huffman tables.
+         *
+         * @return Whether or not this table is one of the predefined "typical"
+         *         Huffman tables.
+         */
+        public boolean isTypical() {
+            if (tableClass == HuffmanTableClass.DC) {
+                return
+                    Arrays.equals(lengthBytes, TYPICAL_LUMINANCE_DC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_LUMINANCE_DC_VALUES) ||
+                    Arrays.equals(lengthBytes, TYPICAL_CHROMINANCE_DC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_CHROMINANCE_DC_VALUES);
+            } else if (tableClass == HuffmanTableClass.AC) {
+                return
+                    Arrays.equals(lengthBytes, TYPICAL_LUMINANCE_AC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_LUMINANCE_AC_VALUES) ||
+                    Arrays.equals(lengthBytes, TYPICAL_CHROMINANCE_AC_LENGTHS) &&
+                    Arrays.equals(valueBytes, TYPICAL_CHROMINANCE_AC_VALUES);
+            }
+            return false;
+        }
+
+        /**
+         * The opposite of {@link #isTypical()}.
+         *
+         * @return Whether or not this table is "optimized" - which means that
+         *         it isn't one of the "typical" Huffman tables.
+         */
+        public boolean isOptimized() {
+            return !isTypical();
+        }
+
+        public enum HuffmanTableClass {
+            DC,
+            AC,
+            UNKNOWN;
+
+            public static HuffmanTableClass typeOf(int value) {
+                switch (value) {
+                    case 0: return DC;
+                    case 1 : return AC;
+                    default: return UNKNOWN;
+                }
+            }
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java b/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java
index c5a67f8..98f7f44 100644
--- a/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java
+++ b/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import com.drew.metadata.TagDescriptor;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegCommentDescriptor extends TagDescriptor<JpegCommentDirectory>
 {
     public JpegCommentDescriptor(@NotNull JpegCommentDirectory directory)
diff --git a/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java b/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java
index 7e077fa..69975de 100644
--- a/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java
+++ b/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegCommentDirectory extends Directory
 {
     /**
diff --git a/Source/com/drew/metadata/jpeg/JpegCommentReader.java b/Source/com/drew/metadata/jpeg/JpegCommentReader.java
index 794c50c..8170ba5 100644
--- a/Source/com/drew/metadata/jpeg/JpegCommentReader.java
+++ b/Source/com/drew/metadata/jpeg/JpegCommentReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,8 +24,9 @@ import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
 import com.drew.imaging.jpeg.JpegSegmentType;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * Decodes the comment stored within JPEG files, populating a {@link Metadata} object with tag values in a
@@ -38,20 +39,17 @@ public class JpegCommentReader implements JpegSegmentMetadataReader
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.COM);
+        return Collections.singletonList(JpegSegmentType.COM);
     }
 
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        // The entire contents of the byte[] is the comment. There's nothing here to discriminate upon.
-        return true;
-    }
-
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
-    {
-        JpegCommentDirectory directory = metadata.getOrCreateDirectory(JpegCommentDirectory.class);
+        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));
+            // The entire contents of the directory are the comment
+            directory.setStringValue(JpegCommentDirectory.TAG_COMMENT, new StringValue(segmentBytes, null));
+        }
     }
 }
diff --git a/Source/com/drew/metadata/jpeg/JpegComponent.java b/Source/com/drew/metadata/jpeg/JpegComponent.java
index 06558ec..7554523 100644
--- a/Source/com/drew/metadata/jpeg/JpegComponent.java
+++ b/Source/com/drew/metadata/jpeg/JpegComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.lang.annotations.Nullable;
+import com.drew.lang.annotations.NotNull;
 
 import java.io.Serializable;
 
@@ -54,7 +54,7 @@ public class JpegComponent implements Serializable
      * Returns the component name (one of: Y, Cb, Cr, I, or Q)
      * @return the component name
      */
-    @Nullable
+    @NotNull
     public String getComponentName()
     {
         switch (_componentId)
@@ -69,8 +69,9 @@ public class JpegComponent implements Serializable
                 return "I";
             case 5:
                 return "Q";
+            default:
+                return String.format("Unknown (%s)", _componentId);
         }
-        return null;
     }
 
     public int getQuantizationTableNumber()
@@ -80,11 +81,22 @@ public class JpegComponent implements Serializable
 
     public int getHorizontalSamplingFactor()
     {
-        return _samplingFactorByte & 0x0F;
+        return (_samplingFactorByte>>4) & 0x0F;
     }
 
     public int getVerticalSamplingFactor()
     {
-        return (_samplingFactorByte>>4) & 0x0F;
+        return _samplingFactorByte & 0x0F;
+    }
+
+    @NotNull
+    @Override
+    public String toString() {
+        return String.format(
+            "Quantization table %d, Sampling factors %d horiz/%d vert",
+            _quantizationTableNumber,
+            getHorizontalSamplingFactor(),
+            getVerticalSamplingFactor()
+        );
     }
 }
diff --git a/Source/com/drew/metadata/jpeg/JpegDescriptor.java b/Source/com/drew/metadata/jpeg/JpegDescriptor.java
index 3989d20..c9aeeb7 100644
--- a/Source/com/drew/metadata/jpeg/JpegDescriptor.java
+++ b/Source/com/drew/metadata/jpeg/JpegDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,12 +24,15 @@ import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.jpeg.JpegDirectory.*;
+
 /**
  * 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
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegDescriptor extends TagDescriptor<JpegDirectory>
 {
     public JpegDescriptor(@NotNull JpegDirectory directory)
@@ -43,21 +46,21 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory>
     {
         switch (tagType)
         {
-            case JpegDirectory.TAG_COMPRESSION_TYPE:
+            case TAG_COMPRESSION_TYPE:
                 return getImageCompressionTypeDescription();
-            case JpegDirectory.TAG_COMPONENT_DATA_1:
+            case TAG_COMPONENT_DATA_1:
                 return getComponentDataDescription(0);
-            case JpegDirectory.TAG_COMPONENT_DATA_2:
+            case TAG_COMPONENT_DATA_2:
                 return getComponentDataDescription(1);
-            case JpegDirectory.TAG_COMPONENT_DATA_3:
+            case TAG_COMPONENT_DATA_3:
                 return getComponentDataDescription(2);
-            case JpegDirectory.TAG_COMPONENT_DATA_4:
+            case TAG_COMPONENT_DATA_4:
                 return getComponentDataDescription(3);
-            case JpegDirectory.TAG_DATA_PRECISION:
+            case TAG_DATA_PRECISION:
                 return getDataPrecisionDescription();
-            case JpegDirectory.TAG_IMAGE_HEIGHT:
+            case TAG_IMAGE_HEIGHT:
                 return getImageHeightDescription();
-            case JpegDirectory.TAG_IMAGE_WIDTH:
+            case TAG_IMAGE_WIDTH:
                 return getImageWidthDescription();
             default:
                 return super.getDescription(tagType);
@@ -67,33 +70,29 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory>
     @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;
-        }
+        return getIndexedDescription(TAG_COMPRESSION_TYPE,
+            "Baseline",
+            "Extended sequential, Huffman",
+            "Progressive, Huffman",
+            "Lossless, Huffman",
+            null, // no 4
+            "Differential sequential, Huffman",
+            "Differential progressive, Huffman",
+            "Differential lossless, Huffman",
+            "Reserved for JPEG extensions",
+            "Extended sequential, arithmetic",
+            "Progressive, arithmetic",
+            "Lossless, arithmetic",
+            null, // no 12
+            "Differential sequential, arithmetic",
+            "Differential progressive, arithmetic",
+            "Differential lossless, arithmetic");
     }
+
     @Nullable
     public String getImageWidthDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_WIDTH);
+        final String value = _directory.getString(TAG_IMAGE_WIDTH);
         if (value==null)
             return null;
         return value + " pixels";
@@ -102,7 +101,7 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory>
     @Nullable
     public String getImageHeightDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_IMAGE_HEIGHT);
+        final String value = _directory.getString(TAG_IMAGE_HEIGHT);
         if (value==null)
             return null;
         return value + " pixels";
@@ -111,7 +110,7 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory>
     @Nullable
     public String getDataPrecisionDescription()
     {
-        final String value = _directory.getString(JpegDirectory.TAG_DATA_PRECISION);
+        final String value = _directory.getString(TAG_DATA_PRECISION);
         if (value==null)
             return null;
         return value + " bits";
@@ -125,8 +124,6 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory>
         if (value==null)
             return null;
 
-        return value.getComponentName() + " component: Quantization table " + value.getQuantizationTableNumber()
-            + ", Sampling factors " + value.getHorizontalSamplingFactor()
-            + " horiz/" + value.getVerticalSamplingFactor() + " vert";
+        return value.getComponentName() + " component: " + value;
     }
 }
diff --git a/Source/com/drew/metadata/jpeg/JpegDhtReader.java b/Source/com/drew/metadata/jpeg/JpegDhtReader.java
new file mode 100644
index 0000000..f8f7691
--- /dev/null
+++ b/Source/com/drew/metadata/jpeg/JpegDhtReader.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Reader for JPEG Huffman tables, found in the DHT JPEG segment.
+ *
+ * @author Nadahar
+ */
+public class JpegDhtReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.DHT);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            extract(new SequentialByteArrayReader(segmentBytes), metadata);
+        }
+    }
+
+    /**
+     * Performs the DHT tables extraction, adding found tables to the specified
+     * instance of {@link Metadata}.
+     */
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        HuffmanTablesDirectory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class);
+        if (directory == null) {
+            directory = new HuffmanTablesDirectory();
+            metadata.addDirectory(directory);
+        }
+
+        try {
+            while (reader.available() > 0) {
+                byte header = reader.getByte();
+                HuffmanTableClass tableClass = HuffmanTableClass.typeOf((header & 0xF0) >> 4);
+                int tableDestinationId = header & 0xF;
+
+                byte[] lBytes = getBytes(reader, 16);
+                int vCount = 0;
+                for (byte b : lBytes) {
+                    vCount += (b & 0xFF);
+                }
+                byte[] vBytes = getBytes(reader, vCount);
+                directory.getTables().add(new HuffmanTable(tableClass, tableDestinationId, lBytes, vBytes));
+            }
+        } catch (IOException me) {
+            directory.addError(me.getMessage());
+        }
+
+        directory.setInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES, directory.getTables().size());
+    }
+
+    private byte[] getBytes(@NotNull final SequentialReader reader, int count) throws IOException {
+        byte[] bytes = new byte[count];
+        for (int i = 0; i < count; i++) {
+            byte b = reader.getByte();
+            if ((b & 0xFF) == 0xFF) {
+                byte stuffing = reader.getByte();
+                if (stuffing != 0x00) {
+                    throw new IOException("Marker " + JpegSegmentType.fromByte(stuffing) + " found inside DHT segment");
+                }
+            }
+            bytes[i] = b;
+        }
+        return bytes;
+    }
+}
diff --git a/Source/com/drew/metadata/jpeg/JpegDirectory.java b/Source/com/drew/metadata/jpeg/JpegDirectory.java
index 37eb488..09a3004 100644
--- a/Source/com/drew/metadata/jpeg/JpegDirectory.java
+++ b/Source/com/drew/metadata/jpeg/JpegDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import java.util.HashMap;
  *
  * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class JpegDirectory extends Directory
 {
     public static final int TAG_COMPRESSION_TYPE = -3;
diff --git a/Source/com/drew/metadata/jpeg/JpegDnlReader.java b/Source/com/drew/metadata/jpeg/JpegDnlReader.java
new file mode 100644
index 0000000..f7ed753
--- /dev/null
+++ b/Source/com/drew/metadata/jpeg/JpegDnlReader.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.ErrorDirectory;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Decodes JPEG DNL data, adjusting the image height with information missing from the JPEG SOFx segment.
+ *
+ * @author Nadahar
+ */
+public class JpegDnlReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.DNL);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            extract(segmentBytes, metadata, segmentType);
+        }
+    }
+
+    public void extract(byte[] segmentBytes, Metadata metadata, JpegSegmentType segmentType)
+    {
+        JpegDirectory directory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
+        if (directory == null) {
+            ErrorDirectory errorDirectory = new ErrorDirectory();
+            metadata.addDirectory(errorDirectory);
+            errorDirectory.addError("DNL segment found without SOFx - illegal JPEG format");
+            return;
+        }
+
+        SequentialReader reader = new SequentialByteArrayReader(segmentBytes);
+
+        try {
+            // Only set height from DNL if it's not already defined
+            Integer i = directory.getInteger(JpegDirectory.TAG_IMAGE_HEIGHT);
+            if (i == null || i == 0) {
+                directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16());
+            }
+        } catch (IOException ex) {
+            directory.addError(ex.getMessage());
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/jpeg/JpegReader.java b/Source/com/drew/metadata/jpeg/JpegReader.java
index 99389fe..9e6bf66 100644
--- a/Source/com/drew/metadata/jpeg/JpegReader.java
+++ b/Source/com/drew/metadata/jpeg/JpegReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -51,7 +51,7 @@ public class JpegReader implements JpegSegmentMetadataReader
             JpegSegmentType.SOF5,
             JpegSegmentType.SOF6,
             JpegSegmentType.SOF7,
-            JpegSegmentType.SOF8,
+//            JpegSegmentType.JPG,
             JpegSegmentType.SOF9,
             JpegSegmentType.SOF10,
             JpegSegmentType.SOF11,
@@ -62,20 +62,17 @@ public class JpegReader implements JpegSegmentMetadataReader
         );
     }
 
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        return true;
+        for (byte[] segmentBytes : segments) {
+            extract(segmentBytes, metadata, segmentType);
+        }
     }
 
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    public void extract(byte[] segmentBytes, Metadata metadata, JpegSegmentType segmentType)
     {
-        if (metadata.containsDirectory(JpegDirectory.class)) {
-            // If this directory is already present, discontinue this operation.
-            // We only store metadata for the *first* matching SOFn segment.
-            return;
-        }
-
-        JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class);
+        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);
@@ -100,7 +97,6 @@ public class JpegReader implements JpegSegmentMetadataReader
                 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/Source/com/drew/metadata/jpeg/package-info.java b/Source/com/drew/metadata/jpeg/package-info.java
new file mode 100644
index 0000000..ad36e22
--- /dev/null
+++ b/Source/com/drew/metadata/jpeg/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of JPEG file format metadata.
+ */
+package com.drew.metadata.jpeg;
diff --git a/Source/com/drew/metadata/jpeg/package.html b/Source/com/drew/metadata/jpeg/package.html
deleted file mode 100644
index 5e9b32a..0000000
--- a/Source/com/drew/metadata/jpeg/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/metadata/package-info.java b/Source/com/drew/metadata/package-info.java
new file mode 100644
index 0000000..2037413
--- /dev/null
+++ b/Source/com/drew/metadata/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Provides classes for generic modelling of metadata directories and tags.
+ * <p />
+ * Contains base types for metadata processing abstraction.
+ */
+package com.drew.metadata;
diff --git a/Source/com/drew/metadata/package.html b/Source/com/drew/metadata/package.html
deleted file mode 100644
index f7b7916..0000000
--- a/Source/com/drew/metadata/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!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/Source/com/drew/metadata/pcx/PcxDescriptor.java b/Source/com/drew/metadata/pcx/PcxDescriptor.java
new file mode 100644
index 0000000..6800624
--- /dev/null
+++ b/Source/com/drew/metadata/pcx/PcxDescriptor.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.pcx;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.pcx.PcxDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PcxDescriptor extends TagDescriptor<PcxDirectory>
+{
+    public PcxDescriptor(@NotNull PcxDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_VERSION:
+                return getVersionDescription();
+            case TAG_COLOR_PLANES:
+                return getColorPlanesDescription();
+            case TAG_PALETTE_TYPE:
+                return getPaletteTypeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getVersionDescription()
+    {
+        // Prior to v2.5 of PC Paintbrush, the PCX image file format was considered proprietary information
+        // by ZSoft Corporation
+
+        return getIndexedDescription(TAG_VERSION,
+            "2.5 with fixed EGA palette information",
+            null,
+            "2.8 with modifiable EGA palette information",
+            "2.8 without palette information (default palette)",
+            "PC Paintbrush for Windows",
+            "3.0 or better");
+    }
+
+    @Nullable
+    public String getColorPlanesDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_PLANES, 3,
+            "24-bit color",
+            "16 colors");
+    }
+
+    @Nullable
+    public String getPaletteTypeDescription()
+    {
+        return getIndexedDescription(TAG_PALETTE_TYPE, 1,
+            "Color or B&W",
+            "Grayscale");
+    }
+}
diff --git a/Source/com/drew/metadata/pcx/PcxDirectory.java b/Source/com/drew/metadata/pcx/PcxDirectory.java
new file mode 100644
index 0000000..5af228f
--- /dev/null
+++ b/Source/com/drew/metadata/pcx/PcxDirectory.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.pcx;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PcxDirectory extends Directory
+{
+    public static final int TAG_VERSION        = 1;
+    public static final int TAG_BITS_PER_PIXEL = 2;
+    public static final int TAG_XMIN           = 3;
+    public static final int TAG_YMIN           = 4;
+    public static final int TAG_XMAX           = 5;
+    public static final int TAG_YMAX           = 6;
+    public static final int TAG_HORIZONTAL_DPI = 7;
+    public static final int TAG_VERTICAL_DPI   = 8;
+    public static final int TAG_PALETTE        = 9;
+    public static final int TAG_COLOR_PLANES   = 10;
+    public static final int TAG_BYTES_PER_LINE = 11;
+    public static final int TAG_PALETTE_TYPE   = 12;
+    public static final int TAG_HSCR_SIZE      = 13;
+    public static final int TAG_VSCR_SIZE      = 14;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_VERSION, "Version");
+        _tagNameMap.put(TAG_BITS_PER_PIXEL, "Bits Per Pixel");
+        _tagNameMap.put(TAG_XMIN, "X Min");
+        _tagNameMap.put(TAG_YMIN, "Y Min");
+        _tagNameMap.put(TAG_XMAX, "X Max");
+        _tagNameMap.put(TAG_YMAX, "Y Max");
+        _tagNameMap.put(TAG_HORIZONTAL_DPI, "Horizontal DPI");
+        _tagNameMap.put(TAG_VERTICAL_DPI, "Vertical DPI");
+        _tagNameMap.put(TAG_PALETTE, "Palette");
+        _tagNameMap.put(TAG_COLOR_PLANES, "Color Planes");
+        _tagNameMap.put(TAG_BYTES_PER_LINE, "Bytes Per Line");
+        _tagNameMap.put(TAG_PALETTE_TYPE, "Palette Type");
+        _tagNameMap.put(TAG_HSCR_SIZE, "H Scr Size");
+        _tagNameMap.put(TAG_VSCR_SIZE, "V Scr Size");
+    }
+
+    public PcxDirectory()
+    {
+        this.setDescriptor(new PcxDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PCX";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/pcx/PcxReader.java b/Source/com/drew/metadata/pcx/PcxReader.java
new file mode 100644
index 0000000..4acf82d
--- /dev/null
+++ b/Source/com/drew/metadata/pcx/PcxReader.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.pcx;
+
+import com.drew.imaging.ImageProcessingException;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+/**
+ * Reads PCX image file metadata.
+ *
+ * <ul>
+ *   <li>https://courses.engr.illinois.edu/ece390/books/labmanual/graphics-pcx.html</li>
+ *   <li>http://www.fileformat.info/format/pcx/egff.htm</li>
+ *   <li>http://fileformats.archiveteam.org/wiki/PCX</li>
+ * </ul>
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PcxReader
+{
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        reader.setMotorolaByteOrder(false);
+
+        PcxDirectory directory = new PcxDirectory();
+        metadata.addDirectory(directory);
+
+        try {
+            byte identifier = reader.getInt8();
+            if (identifier != 0x0A)
+                throw new ImageProcessingException("Invalid PCX identifier byte");
+
+            directory.setInt(PcxDirectory.TAG_VERSION, reader.getInt8());
+
+            byte encoding = reader.getInt8();
+            if (encoding != 0x01)
+                throw new ImageProcessingException("Invalid PCX encoding byte");
+
+            directory.setInt(PcxDirectory.TAG_BITS_PER_PIXEL, reader.getUInt8());
+            directory.setInt(PcxDirectory.TAG_XMIN,           reader.getUInt16());
+            directory.setInt(PcxDirectory.TAG_YMIN,           reader.getUInt16());
+            directory.setInt(PcxDirectory.TAG_XMAX,           reader.getUInt16());
+            directory.setInt(PcxDirectory.TAG_YMAX,           reader.getUInt16());
+            directory.setInt(PcxDirectory.TAG_HORIZONTAL_DPI, reader.getUInt16());
+            directory.setInt(PcxDirectory.TAG_VERTICAL_DPI,   reader.getUInt16());
+            directory.setByteArray(PcxDirectory.TAG_PALETTE,  reader.getBytes(48));
+            reader.skip(1);
+            directory.setInt(PcxDirectory.TAG_COLOR_PLANES,   reader.getUInt8());
+            directory.setInt(PcxDirectory.TAG_BYTES_PER_LINE, reader.getUInt16());
+
+            int paletteType = reader.getUInt16();
+            if (paletteType != 0)
+                directory.setInt(PcxDirectory.TAG_PALETTE_TYPE, paletteType);
+
+            int hScrSize = reader.getUInt16();
+            if (hScrSize != 0)
+                directory.setInt(PcxDirectory.TAG_HSCR_SIZE, hScrSize);
+
+            int vScrSize = reader.getUInt16();
+            if (vScrSize != 0)
+                directory.setInt(PcxDirectory.TAG_VSCR_SIZE, vScrSize);
+
+        } catch (Exception ex) {
+            directory.addError("Exception reading PCX file metadata: " + ex.getMessage());
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/pcx/package-info.java b/Source/com/drew/metadata/pcx/package-info.java
new file mode 100644
index 0000000..0c7bc07
--- /dev/null
+++ b/Source/com/drew/metadata/pcx/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of PCX image file metadata.
+ */
+package com.drew.metadata.pcx;
diff --git a/Source/com/drew/metadata/photoshop/DuckyDirectory.java b/Source/com/drew/metadata/photoshop/DuckyDirectory.java
new file mode 100644
index 0000000..5a9c41c
--- /dev/null
+++ b/Source/com/drew/metadata/photoshop/DuckyDirectory.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.TagDescriptor;
+
+import java.util.HashMap;
+
+/**
+ * Holds the data found in Photoshop "ducky" segments, created during Save-for-Web.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class DuckyDirectory extends Directory
+{
+    public static final int TAG_QUALITY = 1;
+    public static final int TAG_COMMENT = 2;
+    public static final int TAG_COPYRIGHT = 3;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_COMMENT, "Comment");
+        _tagNameMap.put(TAG_COPYRIGHT, "Copyright");
+    }
+
+    public DuckyDirectory()
+    {
+        this.setDescriptor(new TagDescriptor<DuckyDirectory>(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Ducky";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/photoshop/DuckyReader.java b/Source/com/drew/metadata/photoshop/DuckyReader.java
new file mode 100644
index 0000000..f740aca
--- /dev/null
+++ b/Source/com/drew/metadata/photoshop/DuckyReader.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.photoshop;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.Charsets;
+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.Collections;
+
+/**
+ * Reads Photoshop "ducky" segments, created during Save-for-Web.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class DuckyReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    private static final String JPEG_SEGMENT_PREAMBLE = "Ducky";
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.APPC);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
+
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the necessary preamble
+            if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equals(new String(segmentBytes, 0, preambleLength)))
+                continue;
+
+            extract(
+                new SequentialByteArrayReader(segmentBytes, preambleLength),
+                metadata);
+        }
+    }
+
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        DuckyDirectory directory = new DuckyDirectory();
+        metadata.addDirectory(directory);
+
+        try
+        {
+            while (true)
+            {
+                int tag = reader.getUInt16();
+
+                // End of Segment is marked with zero
+                if (tag == 0)
+                    break;
+
+                int length = reader.getUInt16();
+
+                switch (tag)
+                {
+                    case DuckyDirectory.TAG_QUALITY:
+                    {
+                        if (length != 4)
+                        {
+                            directory.addError("Unexpected length for the quality tag");
+                            return;
+                        }
+                        directory.setInt(tag, reader.getInt32());
+                        break;
+                    }
+                    case DuckyDirectory.TAG_COMMENT:
+                    case DuckyDirectory.TAG_COPYRIGHT:
+                    {
+                        reader.skip(4);
+                        directory.setStringValue(tag, reader.getStringValue(length - 4, Charsets.UTF_16BE));
+                        break;
+                    }
+                    default:
+                    {
+                        // Unexpected tag
+                        directory.setByteArray(tag, reader.getBytes(length));
+                        break;
+                    }
+                }
+            }
+        }
+        catch (IOException e)
+        {
+            directory.addError(e.getMessage());
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java b/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java
index 5ca1098..0465641 100644
--- a/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java
+++ b/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -27,11 +27,15 @@ import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 import java.io.IOException;
+import java.text.DecimalFormat;
+
+import static com.drew.metadata.photoshop.PhotoshopDirectory.*;
 
 /**
  * @author Yuri Binev
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
 {
     public PhotoshopDescriptor(@NotNull PhotoshopDirectory directory)
@@ -43,32 +47,32 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case PhotoshopDirectory.TAG_THUMBNAIL:
-            case PhotoshopDirectory.TAG_THUMBNAIL_OLD:
+            case TAG_THUMBNAIL:
+            case TAG_THUMBNAIL_OLD:
                 return getThumbnailDescription(tagType);
-            case PhotoshopDirectory.TAG_URL:
-            case PhotoshopDirectory.TAG_XML:
+            case TAG_URL:
+            case TAG_XML:
                 return getSimpleString(tagType);
-            case PhotoshopDirectory.TAG_IPTC:
+            case TAG_IPTC:
                 return getBinaryDataString(tagType);
-            case PhotoshopDirectory.TAG_SLICES:
+            case TAG_SLICES:
                 return getSlicesDescription();
-            case PhotoshopDirectory.TAG_VERSION:
+            case TAG_VERSION:
                 return getVersionDescription();
-            case PhotoshopDirectory.TAG_COPYRIGHT:
+            case TAG_COPYRIGHT:
                 return getBooleanString(tagType);
-            case PhotoshopDirectory.TAG_RESOLUTION_INFO:
+            case TAG_RESOLUTION_INFO:
                 return getResolutionInfoDescription();
-            case PhotoshopDirectory.TAG_GLOBAL_ANGLE:
-            case PhotoshopDirectory.TAG_GLOBAL_ALTITUDE:
-            case PhotoshopDirectory.TAG_URL_LIST:
-            case PhotoshopDirectory.TAG_SEED_NUMBER:
+            case TAG_GLOBAL_ANGLE:
+            case TAG_GLOBAL_ALTITUDE:
+            case TAG_URL_LIST:
+            case TAG_SEED_NUMBER:
                 return get32BitNumberString(tagType);
-            case PhotoshopDirectory.TAG_JPEG_QUALITY:
+            case TAG_JPEG_QUALITY:
                 return getJpegQualityString();
-            case PhotoshopDirectory.TAG_PRINT_SCALE:
+            case TAG_PRINT_SCALE:
                 return getPrintScaleDescription();
-            case PhotoshopDirectory.TAG_PIXEL_ASPECT_RATIO:
+            case TAG_PIXEL_ASPECT_RATIO:
                 return getPixelAspectRatioString();
             default:
                 return super.getDescription(tagType);
@@ -79,21 +83,21 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getJpegQualityString()
     {
         try {
-            byte[] b = _directory.getByteArray(PhotoshopDirectory.TAG_JPEG_QUALITY);
+            byte[] b = _directory.getByteArray(TAG_JPEG_QUALITY);
+
             if (b == null)
-                return _directory.getString(PhotoshopDirectory.TAG_JPEG_QUALITY);
+                return _directory.getString(TAG_JPEG_QUALITY);
+
             RandomAccessReader reader = new ByteArrayReader(b);
             int q = reader.getUInt16(0); // & 0xFFFF;
             int f = reader.getUInt16(2); // & 0xFFFF;
             int s = reader.getUInt16(4);
 
-            int q1;
-            if (q <= 0xFFFF && q >= 0xFFFD)
-                q1 = q - 0xFFFC;
-            else if (q <= 8)
-                q1 = q + 4;
-            else
-                q1 = q;
+            int q1 = q <= 0xFFFF && q >= 0xFFFD
+                ? q - 0xFFFC
+                : q <= 8
+                    ? q + 4
+                    : q;
 
             String quality;
             switch (q) {
@@ -120,6 +124,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
                 default:
                     quality = "Unknown";
             }
+
             String format;
             switch (f) {
                 case 0x0000:
@@ -129,14 +134,16 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
                     format = "Optimised";
                     break;
                 case 0x0101:
-                    format = "Progressive ";
+                    format = "Progressive";
                     break;
                 default:
                     format = String.format("Unknown 0x%04X", f);
             }
+
             String scans = s >= 1 && s <= 3
                     ? String.format("%d", s + 2)
                     : String.format("Unknown 0x%04X", s);
+
             return String.format("%d (%s), %s format, %s scans", q1, quality, format, scans);
         } catch (IOException e) {
             return null;
@@ -147,7 +154,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getPixelAspectRatioString()
     {
         try {
-            byte[] bytes = _directory.getByteArray(PhotoshopDirectory.TAG_PIXEL_ASPECT_RATIO);
+            byte[] bytes = _directory.getByteArray(TAG_PIXEL_ASPECT_RATIO);
             if (bytes == null)
                 return null;
             RandomAccessReader reader = new ByteArrayReader(bytes);
@@ -162,7 +169,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getPrintScaleDescription()
     {
         try {
-            byte bytes[] = _directory.getByteArray(PhotoshopDirectory.TAG_PRINT_SCALE);
+            byte bytes[] = _directory.getByteArray(TAG_PRINT_SCALE);
             if (bytes == null)
                 return null;
             RandomAccessReader reader = new ByteArrayReader(bytes);
@@ -189,13 +196,14 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getResolutionInfoDescription()
     {
         try {
-            byte[] bytes = _directory.getByteArray(PhotoshopDirectory.TAG_RESOLUTION_INFO);
+            byte[] bytes = _directory.getByteArray(TAG_RESOLUTION_INFO);
             if (bytes == null)
                 return null;
             RandomAccessReader reader = new ByteArrayReader(bytes);
             float resX = reader.getS15Fixed16(0);
             float resY = reader.getS15Fixed16(8); // is this the correct offset? it's only reading 4 bytes each time
-            return resX + "x" + resY + " DPI";
+            DecimalFormat format = new DecimalFormat("0.##");
+            return format.format(resX) + "x" + format.format(resY) + " DPI";
         } catch (Exception e) {
             return null;
         }
@@ -205,7 +213,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getVersionDescription()
     {
         try {
-            final byte[] bytes = _directory.getByteArray(PhotoshopDirectory.TAG_VERSION);
+            final byte[] bytes = _directory.getByteArray(TAG_VERSION);
             if (bytes == null)
                 return null;
             RandomAccessReader reader = new ByteArrayReader(bytes);
@@ -232,7 +240,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     public String getSlicesDescription()
     {
         try {
-            final byte bytes[] = _directory.getByteArray(PhotoshopDirectory.TAG_SLICES);
+            final byte bytes[] = _directory.getByteArray(TAG_SLICES);
             if (bytes == null)
                 return null;
             RandomAccessReader reader = new ByteArrayReader(bytes);
@@ -240,16 +248,8 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
             String name = reader.getString(24, nameLength * 2, "UTF-16");
             int pos = 24 + nameLength * 2;
             int sliceCount = reader.getInt32(pos);
-            //pos += 4;
             return String.format("%s (%d,%d,%d,%d) %d Slices",
                     name, reader.getInt32(4), reader.getInt32(8), reader.getInt32(12), reader.getInt32(16), sliceCount);
-            /*for (int i=0;i<sliceCount;i++){
-                pos+=16;
-                int slNameLen=getInt32(b,pos);
-                pos+=4;
-                String slName=new String(b, pos, slNameLen*2,"UTF-16");
-                res+=slName;
-            }*/
         } catch (IOException e) {
             return null;
         }
@@ -263,22 +263,14 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
             if (v == null)
                 return null;
             RandomAccessReader reader = new ByteArrayReader(v);
-            //int pos = 0;
             int format = reader.getInt32(0);
-            //pos += 4;
             int width = reader.getInt32(4);
-            //pos += 4;
             int height = reader.getInt32(8);
-            //pos += 4;
-            //pos += 4; //skip WidthBytes
+            //skip WidthBytes
             int totalSize = reader.getInt32(16);
-            //pos += 4;
             int compSize = reader.getInt32(20);
-            //pos += 4;
             int bpp = reader.getInt32(24);
-            //pos+=2;
-            //pos+=2; //skip Number of planes
-            //int thumbSize=v.length-pos;
+            //skip Number of planes
             return String.format("%s, %dx%d, Decomp %d bytes, %d bpp, %d bytes",
                     format == 1 ? "JpegRGB" : "RawRGB",
                     width, height, totalSize, bpp, compSize);
@@ -291,7 +283,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
     private String getBooleanString(int tag)
     {
         final byte[] bytes = _directory.getByteArray(tag);
-        if (bytes == null)
+        if (bytes == null || bytes.length == 0)
             return null;
         return bytes[0] == 0 ? "No" : "Yes";
     }
diff --git a/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java b/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java
index 7e80ecc..bd20fde 100644
--- a/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java
+++ b/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,53 +30,100 @@ import java.util.HashMap;
 /**
  * Holds the metadata found in the APPD segment of a JPEG file saved by Photoshop.
  *
- * @author Yuri Binev, Drew Noakes https://drewnoakes.com
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Yuri Binev
  */
+@SuppressWarnings("WeakerAccess")
 public class PhotoshopDirectory extends Directory
 {
-    public static final int TAG_CHANNELS_ROWS_COLUMNS_DEPTH_MODE = 0x03E8;
-    public static final int TAG_MAC_PRINT_INFO = 0x03E9;
-    public static final int TAG_XML = 0x03EA;
-    public static final int TAG_INDEXED_COLOR_TABLE = 0x03EB;
-    public static final int TAG_RESOLUTION_INFO = 0x03ED;
-    public static final int TAG_ALPHA_CHANNELS = 0x03EE;
-    public static final int TAG_DISPLAY_INFO = 0x03EF;
-    public static final int TAG_CAPTION = 0x03F0;
-    public static final int TAG_BORDER_INFORMATION = 0x03F1;
-    public static final int TAG_BACKGROUND_COLOR = 0x03F2;
-    public static final int TAG_PRINT_FLAGS = 0x03F3;
+    public static final int TAG_CHANNELS_ROWS_COLUMNS_DEPTH_MODE                  = 0x03E8;
+    public static final int TAG_MAC_PRINT_INFO                                    = 0x03E9;
+    public static final int TAG_XML                                               = 0x03EA;
+    public static final int TAG_INDEXED_COLOR_TABLE                               = 0x03EB;
+    public static final int TAG_RESOLUTION_INFO                                   = 0x03ED;
+    public static final int TAG_ALPHA_CHANNELS                                    = 0x03EE;
+    public static final int TAG_DISPLAY_INFO_OBSOLETE                             = 0x03EF;
+    public static final int TAG_CAPTION                                           = 0x03F0;
+    public static final int TAG_BORDER_INFORMATION                                = 0x03F1;
+    public static final int TAG_BACKGROUND_COLOR                                  = 0x03F2;
+    public static final int TAG_PRINT_FLAGS                                       = 0x03F3;
     public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_HALFTONING_INFORMATION = 0x03F4;
-    public static final int TAG_COLOR_HALFTONING_INFORMATION = 0x03F5;
-    public static final int TAG_DUOTONE_HALFTONING_INFORMATION = 0x03F6;
-    public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_TRANSFER_FUNCTION = 0x03F7;
-    public static final int TAG_COLOR_TRANSFER_FUNCTIONS = 0x03F8;
-    public static final int TAG_DUOTONE_TRANSFER_FUNCTIONS = 0x03F9;
-    public static final int TAG_DUOTONE_IMAGE_INFORMATION = 0x03FA;
-    public static final int TAG_EFFECTIVE_BLACK_AND_WHITE_VALUES = 0x03FB;
-    public static final int TAG_EPS_OPTIONS = 0x03FD;
-    public static final int TAG_QUICK_MASK_INFORMATION = 0x03FE;
-    public static final int TAG_LAYER_STATE_INFORMATION = 0x0400;
-    public static final int TAG_LAYERS_GROUP_INFORMATION = 0x0402;
-    public static final int TAG_IPTC = 0x0404;
-    public static final int TAG_IMAGE_MODE_FOR_RAW_FORMAT_FILES = 0x0405;
-    public static final int TAG_JPEG_QUALITY = 0x0406;
-    public static final int TAG_GRID_AND_GUIDES_INFORMATION = 0x0408;
-    public static final int TAG_THUMBNAIL_OLD = 0x0409;
-    public static final int TAG_COPYRIGHT = 0x040A;
-    public static final int TAG_URL = 0x040B;
-    public static final int TAG_THUMBNAIL = 0x040C;
-    public static final int TAG_GLOBAL_ANGLE = 0x040D;
-    public static final int TAG_ICC_UNTAGGED_PROFILE = 0x0411;
-    public static final int TAG_SEED_NUMBER = 0x0414;
-    public static final int TAG_GLOBAL_ALTITUDE = 0x0419;
-    public static final int TAG_SLICES = 0x041A;
-    public static final int TAG_URL_LIST = 0x041E;
-    public static final int TAG_VERSION = 0x0421;
-    public static final int TAG_CAPTION_DIGEST = 0x0425;
-    public static final int TAG_PRINT_SCALE = 0x0426;
-    public static final int TAG_PIXEL_ASPECT_RATIO = 0x0428;
-    public static final int TAG_PRINT_INFO = 0x042F;
-    public static final int TAG_PRINT_FLAGS_INFO = 0x2710;
+    public static final int TAG_COLOR_HALFTONING_INFORMATION                      = 0x03F5;
+    public static final int TAG_DUOTONE_HALFTONING_INFORMATION                    = 0x03F6;
+    public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_TRANSFER_FUNCTION      = 0x03F7;
+    public static final int TAG_COLOR_TRANSFER_FUNCTIONS                          = 0x03F8;
+    public static final int TAG_DUOTONE_TRANSFER_FUNCTIONS                        = 0x03F9;
+    public static final int TAG_DUOTONE_IMAGE_INFORMATION                         = 0x03FA;
+    public static final int TAG_EFFECTIVE_BLACK_AND_WHITE_VALUES                  = 0x03FB;
+    // OBSOLETE                                                                     0x03FC
+    public static final int TAG_EPS_OPTIONS                                       = 0x03FD;
+    public static final int TAG_QUICK_MASK_INFORMATION                            = 0x03FE;
+    // OBSOLETE                                                                     0x03FF
+    public static final int TAG_LAYER_STATE_INFORMATION                           = 0x0400;
+    // Working path (not saved)                                                     0x0401
+    public static final int TAG_LAYERS_GROUP_INFORMATION                          = 0x0402;
+    // OBSOLETE                                                                     0x0403
+    public static final int TAG_IPTC                                              = 0x0404;
+    public static final int TAG_IMAGE_MODE_FOR_RAW_FORMAT_FILES                   = 0x0405;
+    public static final int TAG_JPEG_QUALITY                                      = 0x0406;
+    public static final int TAG_GRID_AND_GUIDES_INFORMATION                       = 0x0408;
+    public static final int TAG_THUMBNAIL_OLD                                     = 0x0409;
+    public static final int TAG_COPYRIGHT                                         = 0x040A;
+    public static final int TAG_URL                                               = 0x040B;
+    public static final int TAG_THUMBNAIL                                         = 0x040C;
+    public static final int TAG_GLOBAL_ANGLE                                      = 0x040D;
+    // OBSOLETE                                                                     0x040E
+    public static final int TAG_ICC_PROFILE_BYTES                                 = 0x040F;
+    public static final int TAG_WATERMARK                                         = 0x0410;
+    public static final int TAG_ICC_UNTAGGED_PROFILE                              = 0x0411;
+    public static final int TAG_EFFECTS_VISIBLE                                   = 0x0412;
+    public static final int TAG_SPOT_HALFTONE                                     = 0x0413;
+    public static final int TAG_SEED_NUMBER                                       = 0x0414;
+    public static final int TAG_UNICODE_ALPHA_NAMES                               = 0x0415;
+    public static final int TAG_INDEXED_COLOR_TABLE_COUNT                         = 0x0416;
+    public static final int TAG_TRANSPARENCY_INDEX                                = 0x0417;
+    public static final int TAG_GLOBAL_ALTITUDE                                   = 0x0419;
+    public static final int TAG_SLICES                                            = 0x041A;
+    public static final int TAG_WORKFLOW_URL                                      = 0x041B;
+    public static final int TAG_JUMP_TO_XPEP                                      = 0x041C;
+    public static final int TAG_ALPHA_IDENTIFIERS                                 = 0x041D;
+    public static final int TAG_URL_LIST                                          = 0x041E;
+    public static final int TAG_VERSION                                           = 0x0421;
+    public static final int TAG_EXIF_DATA_1                                       = 0x0422;
+    public static final int TAG_EXIF_DATA_3                                       = 0x0423;
+    public static final int TAG_XMP_DATA                                          = 0x0424;
+    public static final int TAG_CAPTION_DIGEST                                    = 0x0425;
+    public static final int TAG_PRINT_SCALE                                       = 0x0426;
+    public static final int TAG_PIXEL_ASPECT_RATIO                                = 0x0428;
+    public static final int TAG_LAYER_COMPS                                       = 0x0429;
+    public static final int TAG_ALTERNATE_DUOTONE_COLORS                          = 0x042A;
+    public static final int TAG_ALTERNATE_SPOT_COLORS                             = 0x042B;
+    public static final int TAG_LAYER_SELECTION_IDS                               = 0x042D;
+    public static final int TAG_HDR_TONING_INFO                                   = 0x042E;
+    public static final int TAG_PRINT_INFO                                        = 0x042F;
+    public static final int TAG_LAYER_GROUPS_ENABLED_ID                           = 0x0430;
+    public static final int TAG_COLOR_SAMPLERS                                    = 0x0431;
+    public static final int TAG_MEASUREMENT_SCALE                                 = 0x0432;
+    public static final int TAG_TIMELINE_INFORMATION                              = 0x0433;
+    public static final int TAG_SHEET_DISCLOSURE                                  = 0x0434;
+    public static final int TAG_DISPLAY_INFO                                      = 0x0435;
+    public static final int TAG_ONION_SKINS                                       = 0x0436;
+    public static final int TAG_COUNT_INFORMATION                                 = 0x0438;
+    public static final int TAG_PRINT_INFO_2                                      = 0x043A;
+    public static final int TAG_PRINT_STYLE                                       = 0x043B;
+    public static final int TAG_MAC_NSPRINTINFO                                   = 0x043C;
+    public static final int TAG_WIN_DEVMODE                                       = 0x043D;
+    public static final int TAG_AUTO_SAVE_FILE_PATH                               = 0x043E;
+    public static final int TAG_AUTO_SAVE_FORMAT                                  = 0x043F;
+    public static final int TAG_PATH_SELECTION_STATE                              = 0x0440;
+    // CLIPPING PATHS                                                               0x07D0 -> 0x0BB6
+    public static final int TAG_CLIPPING_PATH_NAME                                = 0x0BB7;
+    public static final int TAG_ORIGIN_PATH_INFO                                  = 0x0BB8;
+    // PLUG IN RESOURCES                                                            0x0FA0 -> 0x1387
+    public static final int TAG_IMAGE_READY_VARIABLES_XML                         = 0x1B58;
+    public static final int TAG_IMAGE_READY_DATA_SETS                             = 0x1B59;
+    public static final int TAG_LIGHTROOM_WORKFLOW                                = 0x1F40;
+    public static final int TAG_PRINT_FLAGS_INFO                                  = 0x2710;
 
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
@@ -88,7 +135,7 @@ public class PhotoshopDirectory extends Directory
         _tagNameMap.put(TAG_INDEXED_COLOR_TABLE, "Indexed Color Table");
         _tagNameMap.put(TAG_RESOLUTION_INFO, "Resolution Info");
         _tagNameMap.put(TAG_ALPHA_CHANNELS, "Alpha Channels");
-        _tagNameMap.put(TAG_DISPLAY_INFO, "Display Info");
+        _tagNameMap.put(TAG_DISPLAY_INFO_OBSOLETE, "Display Info (Obsolete)");
         _tagNameMap.put(TAG_CAPTION, "Caption");
         _tagNameMap.put(TAG_BORDER_INFORMATION, "Border Information");
         _tagNameMap.put(TAG_BACKGROUND_COLOR, "Background Color");
@@ -114,16 +161,54 @@ public class PhotoshopDirectory extends Directory
         _tagNameMap.put(TAG_URL, "URL");
         _tagNameMap.put(TAG_THUMBNAIL, "Thumbnail Data");
         _tagNameMap.put(TAG_GLOBAL_ANGLE, "Global Angle");
+        _tagNameMap.put(TAG_ICC_PROFILE_BYTES, "ICC Profile Bytes");
+        _tagNameMap.put(TAG_WATERMARK, "Watermark");
         _tagNameMap.put(TAG_ICC_UNTAGGED_PROFILE, "ICC Untagged Profile");
+        _tagNameMap.put(TAG_EFFECTS_VISIBLE, "Effects Visible");
+        _tagNameMap.put(TAG_SPOT_HALFTONE, "Spot Halftone");
         _tagNameMap.put(TAG_SEED_NUMBER, "Seed Number");
+        _tagNameMap.put(TAG_UNICODE_ALPHA_NAMES, "Unicode Alpha Names");
+        _tagNameMap.put(TAG_INDEXED_COLOR_TABLE_COUNT, "Indexed Color Table Count");
+        _tagNameMap.put(TAG_TRANSPARENCY_INDEX, "Transparency Index");
         _tagNameMap.put(TAG_GLOBAL_ALTITUDE, "Global Altitude");
         _tagNameMap.put(TAG_SLICES, "Slices");
+        _tagNameMap.put(TAG_WORKFLOW_URL, "Workflow URL");
+        _tagNameMap.put(TAG_JUMP_TO_XPEP, "Jump To XPEP");
+        _tagNameMap.put(TAG_ALPHA_IDENTIFIERS, "Alpha Identifiers");
         _tagNameMap.put(TAG_URL_LIST, "URL List");
         _tagNameMap.put(TAG_VERSION, "Version Info");
+        _tagNameMap.put(TAG_EXIF_DATA_1, "EXIF Data 1");
+        _tagNameMap.put(TAG_EXIF_DATA_3, "EXIF Data 3");
+        _tagNameMap.put(TAG_XMP_DATA, "XMP Data");
         _tagNameMap.put(TAG_CAPTION_DIGEST, "Caption Digest");
         _tagNameMap.put(TAG_PRINT_SCALE, "Print Scale");
         _tagNameMap.put(TAG_PIXEL_ASPECT_RATIO, "Pixel Aspect Ratio");
+        _tagNameMap.put(TAG_LAYER_COMPS, "Layer Comps");
+        _tagNameMap.put(TAG_ALTERNATE_DUOTONE_COLORS, "Alternate Duotone Colors");
+        _tagNameMap.put(TAG_ALTERNATE_SPOT_COLORS, "Alternate Spot Colors");
+        _tagNameMap.put(TAG_LAYER_SELECTION_IDS, "Layer Selection IDs");
+        _tagNameMap.put(TAG_HDR_TONING_INFO, "HDR Toning Info");
         _tagNameMap.put(TAG_PRINT_INFO, "Print Info");
+        _tagNameMap.put(TAG_LAYER_GROUPS_ENABLED_ID, "Layer Groups Enabled ID");
+        _tagNameMap.put(TAG_COLOR_SAMPLERS, "Color Samplers");
+        _tagNameMap.put(TAG_MEASUREMENT_SCALE, "Measurement Scale");
+        _tagNameMap.put(TAG_TIMELINE_INFORMATION, "Timeline Information");
+        _tagNameMap.put(TAG_SHEET_DISCLOSURE, "Sheet Disclosure");
+        _tagNameMap.put(TAG_DISPLAY_INFO, "Display Info");
+        _tagNameMap.put(TAG_ONION_SKINS, "Onion Skins");
+        _tagNameMap.put(TAG_COUNT_INFORMATION, "Count information");
+        _tagNameMap.put(TAG_PRINT_INFO_2, "Print Info 2");
+        _tagNameMap.put(TAG_PRINT_STYLE, "Print Style");
+        _tagNameMap.put(TAG_MAC_NSPRINTINFO, "Mac NSPrintInfo");
+        _tagNameMap.put(TAG_WIN_DEVMODE, "Win DEVMODE");
+        _tagNameMap.put(TAG_AUTO_SAVE_FILE_PATH, "Auto Save File Path");
+        _tagNameMap.put(TAG_AUTO_SAVE_FORMAT, "Auto Save Format");
+        _tagNameMap.put(TAG_PATH_SELECTION_STATE, "Path Selection State");
+        _tagNameMap.put(TAG_CLIPPING_PATH_NAME, "Clipping Path Name");
+        _tagNameMap.put(TAG_ORIGIN_PATH_INFO, "Origin Path Info");
+        _tagNameMap.put(TAG_IMAGE_READY_VARIABLES_XML, "Image Ready Variables XML");
+        _tagNameMap.put(TAG_IMAGE_READY_DATA_SETS, "Image Ready Data Sets");
+        _tagNameMap.put(TAG_LIGHTROOM_WORKFLOW, "Lightroom Workflow");
         _tagNameMap.put(TAG_PRINT_FLAGS_INFO, "Print Flags Information");
     }
 
@@ -152,7 +237,7 @@ public class PhotoshopDirectory extends Directory
         byte[] storedBytes = getByteArray(PhotoshopDirectory.TAG_THUMBNAIL);
         if (storedBytes == null)
             storedBytes = getByteArray(PhotoshopDirectory.TAG_THUMBNAIL_OLD);
-        if (storedBytes == null)
+        if (storedBytes == null || storedBytes.length <= 28)
             return null;
 
         int thumbSize = storedBytes.length - 28;
diff --git a/Source/com/drew/metadata/photoshop/PhotoshopReader.java b/Source/com/drew/metadata/photoshop/PhotoshopReader.java
index 1069e06..d8ff7e2 100644
--- a/Source/com/drew/metadata/photoshop/PhotoshopReader.java
+++ b/Source/com/drew/metadata/photoshop/PhotoshopReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -20,105 +20,125 @@
  */
 package com.drew.metadata.photoshop;
 
+import com.drew.imaging.ImageProcessingException;
 import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
 import com.drew.imaging.jpeg.JpegSegmentType;
 import com.drew.lang.ByteArrayReader;
-import com.drew.lang.RandomAccessReader;
 import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataReader;
+import com.drew.metadata.exif.ExifReader;
+import com.drew.metadata.icc.IccReader;
 import com.drew.metadata.iptc.IptcReader;
+import com.drew.metadata.xmp.XmpReader;
 
-import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * Reads metadata created by Photoshop and stored in the APPD segment of JPEG files.
  * Note that IPTC data may be stored within this segment, in which case this reader will
  * create both a {@link PhotoshopDirectory} and a {@link com.drew.metadata.iptc.IptcDirectory}.
  *
- * @author Yuri Binev, Drew Noakes https://drewnoakes.com
+ * @author Yuri Binev
+ * @author Drew Noakes https://drewnoakes.com
  */
-public class PhotoshopReader implements JpegSegmentMetadataReader, MetadataReader
+public class PhotoshopReader implements JpegSegmentMetadataReader
 {
+    @NotNull
+    private static final String JPEG_SEGMENT_PREAMBLE = "Photoshop 3.0";
+
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APPD);
+        return Collections.singletonList(JpegSegmentType.APPD);
     }
 
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        return segmentBytes.length > 12 && "Photoshop 3.0".equals(new String(segmentBytes, 0, 13));
-    }
+        final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
 
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
-    {
-        extract(new ByteArrayReader(segmentBytes), metadata);
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the necessary preamble
+            if (segmentBytes.length < preambleLength + 1 || !JPEG_SEGMENT_PREAMBLE.equals(new String(segmentBytes, 0, preambleLength)))
+                continue;
+
+            extract(
+                new SequentialByteArrayReader(segmentBytes, preambleLength + 1),
+                segmentBytes.length - preambleLength - 1,
+                metadata);
+        }
     }
 
-    public void extract(@NotNull final RandomAccessReader reader, final @NotNull Metadata metadata)
+    public void extract(@NotNull final SequentialReader reader, int length, @NotNull final Metadata metadata)
     {
-        final PhotoshopDirectory directory = metadata.getOrCreateDirectory(PhotoshopDirectory.class);
+        PhotoshopDirectory directory = new PhotoshopDirectory();
+        metadata.addDirectory(directory);
 
-        int pos;
-        try {
-            pos = reader.getString(0, 13).equals("Photoshop 3.0") ? 14 : 0;
-        } catch (IOException e) {
-            directory.addError("Unable to read header");
-            return;
-        }
-
-        long length;
-        try {
-            length = reader.getLength();
-        } catch (IOException e) {
-            directory.addError("Unable to read Photoshop data: " + e.getMessage());
-            return;
-        }
+        // Data contains a sequence of Image Resource Blocks (IRBs):
+        //
+        // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found
+        // 2 bytes - Resource identifier
+        // String  - Pascal string, padded to make length even
+        // 4 bytes - Size of resource data which follows
+        // Data    - The resource data, padded to make size even
+        //
+        // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
 
+        int pos = 0;
         while (pos < length) {
             try {
-                // 4 bytes for the signature.  Should always be "8BIM".
-                //String signature = new String(data, pos, 4);
+                // 4 bytes for the signature ("8BIM", "PHUT", etc.)
+                String signature = reader.getString(4);
                 pos += 4;
 
                 // 2 bytes for the resource identifier (tag type).
-                int tagType = reader.getUInt16(pos); // segment type
+                int tagType = reader.getUInt16(); // segment type
                 pos += 2;
 
                 // A variable number of bytes holding a pascal string (two leading bytes for length).
-                int descriptionLength = reader.getUInt16(pos);
-                pos += 2;
+                short descriptionLength = reader.getUInt8();
+                pos += 1;
                 // Some basic bounds checking
                 if (descriptionLength < 0 || descriptionLength + pos > length)
-                    return;
-                //String description = new String(data, pos, descriptionLength);
+                    throw new ImageProcessingException("Invalid string length");
+                // We don't use the string value here
+                reader.skip(descriptionLength);
                 pos += descriptionLength;
                 // The number of bytes is padded with a trailing zero, if needed, to make the size even.
-                if (pos % 2 != 0)
+                if (pos % 2 != 0) {
+                    reader.skip(1);
                     pos++;
+                }
 
                 // 4 bytes for the size of the resource data that follows.
-                int byteCount = reader.getInt32(pos);
+                int byteCount = reader.getInt32();
                 pos += 4;
                 // The resource data.
-                byte[] tagBytes = reader.getBytes(pos, byteCount);
+                byte[] tagBytes = reader.getBytes(byteCount);
                 pos += byteCount;
                 // The number of bytes is padded with a trailing zero, if needed, to make the size even.
-                if (pos % 2 != 0)
+                if (pos % 2 != 0) {
+                    reader.skip(1);
                     pos++;
+                }
 
-                directory.setByteArray(tagType, tagBytes);
-
-                // TODO allow rebasing the reader with a new zero-point, rather than copying data here
-                if (tagType == PhotoshopDirectory.TAG_IPTC)
-                    new IptcReader().extract(new SequentialByteArrayReader(tagBytes), metadata, tagBytes.length);
+                if (signature.equals("8BIM")) {
+                    if (tagType == PhotoshopDirectory.TAG_IPTC)
+                        new IptcReader().extract(new SequentialByteArrayReader(tagBytes), metadata, tagBytes.length, directory);
+                    else if (tagType == PhotoshopDirectory.TAG_ICC_PROFILE_BYTES)
+                        new IccReader().extract(new ByteArrayReader(tagBytes), metadata, directory);
+                    else if (tagType == PhotoshopDirectory.TAG_EXIF_DATA_1 || tagType == PhotoshopDirectory.TAG_EXIF_DATA_3)
+                        new ExifReader().extract(new ByteArrayReader(tagBytes), metadata, 0, directory);
+                    else if (tagType == PhotoshopDirectory.TAG_XMP_DATA)
+                        new XmpReader().extract(tagBytes, metadata, directory);
+                    else
+                        directory.setByteArray(tagType, tagBytes);
 
-                if (tagType >= 0x0fa0 && tagType <= 0x1387)
-                    PhotoshopDirectory._tagNameMap.put(tagType, String.format("Plug-in %d Data", tagType - 0x0fa0 + 1));
-            } catch (IOException ex) {
+                    if (tagType >= 0x0fa0 && tagType <= 0x1387)
+                        PhotoshopDirectory._tagNameMap.put(tagType, String.format("Plug-in %d Data", tagType - 0x0fa0 + 1));
+                }
+            } catch (Exception ex) {
                 directory.addError(ex.getMessage());
                 return;
             }
diff --git a/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java b/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java
index d14c235..b651ec9 100644
--- a/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java
+++ b/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -25,9 +25,12 @@ import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import static com.drew.metadata.photoshop.PsdHeaderDirectory.*;
+
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory>
 {
     public PsdHeaderDescriptor(@NotNull PsdHeaderDirectory directory)
@@ -39,15 +42,15 @@ public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case PsdHeaderDirectory.TAG_CHANNEL_COUNT:
+            case TAG_CHANNEL_COUNT:
                 return getChannelCountDescription();
-            case PsdHeaderDirectory.TAG_BITS_PER_CHANNEL:
+            case TAG_BITS_PER_CHANNEL:
                 return getBitsPerChannelDescription();
-            case PsdHeaderDirectory.TAG_COLOR_MODE:
+            case TAG_COLOR_MODE:
                 return getColorModeDescription();
-            case PsdHeaderDirectory.TAG_IMAGE_HEIGHT:
+            case TAG_IMAGE_HEIGHT:
                 return getImageHeightDescription();
-            case PsdHeaderDirectory.TAG_IMAGE_WIDTH:
+            case TAG_IMAGE_WIDTH:
                 return getImageWidthDescription();
             default:
                 return super.getDescription(tagType);
@@ -57,71 +60,53 @@ public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory>
     @Nullable
     public String getChannelCountDescription()
     {
-        try {
-            Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_CHANNEL_COUNT);
-            if (value == null)
-                return null;
-            return value + " channel" + (value == 1 ? "" : "s");
-        } catch (Exception e) {
+        // Supported range is 1 to 56.
+        Integer value = _directory.getInteger(TAG_CHANNEL_COUNT);
+        if (value == null)
             return null;
-        }
+        return value + " channel" + (value == 1 ? "" : "s");
     }
 
     @Nullable
     public String getBitsPerChannelDescription()
     {
-        try {
-            Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL);
-            if (value == null)
-                return null;
-            return value + " bit" + (value == 1 ? "" : "s") + " per channel";
-        } catch (Exception e) {
+        // Supported values are 1, 8, 16 and 32.
+        Integer value = _directory.getInteger(TAG_BITS_PER_CHANNEL);
+        if (value == null)
             return null;
-        }
+        return value + " bit" + (value == 1 ? "" : "s") + " per channel";
     }
 
     @Nullable
     public String getColorModeDescription()
     {
-        // Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9
-        try {
-            Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_COLOR_MODE);
-            if (value == null)
-                return null;
-            switch (value){
-                case 0: return "Bitmap";
-                case 1: return "Grayscale";
-                case 2: return "Indexed";
-                case 3: return "RGB";
-                case 4: return "CMYK";
-                case 7: return "Multichannel";
-                case 8: return "Duotone";
-                case 9: return "Lab";
-                default: return "Unknown color mode (" + value + ")";
-            }
-        } catch (Exception e) {
-            return null;
-        }
+        return getIndexedDescription(TAG_COLOR_MODE,
+            "Bitmap",
+            "Grayscale",
+            "Indexed",
+            "RGB",
+            "CMYK",
+            null,
+            null,
+            "Multichannel",
+            "Duotone",
+            "Lab");
     }
 
     @Nullable
     public String getImageHeightDescription()
     {
-        try {
-            Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_IMAGE_HEIGHT);
-            if (value == null)
-                return null;
-            return value + " pixel" + (value == 1 ? "" : "s");
-        } catch (Exception e) {
+        Integer value = _directory.getInteger(TAG_IMAGE_HEIGHT);
+        if (value == null)
             return null;
-        }
+        return value + " pixel" + (value == 1 ? "" : "s");
     }
 
     @Nullable
     public String getImageWidthDescription()
     {
         try {
-            Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_IMAGE_WIDTH);
+            Integer value = _directory.getInteger(TAG_IMAGE_WIDTH);
             if (value == null)
                 return null;
             return value + " pixel" + (value == 1 ? "" : "s");
diff --git a/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java b/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java
index ad38774..43bbe5e 100644
--- a/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java
+++ b/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ import java.util.HashMap;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PsdHeaderDirectory extends Directory
 {
     /**
diff --git a/Source/com/drew/metadata/photoshop/PsdReader.java b/Source/com/drew/metadata/photoshop/PsdReader.java
index fe43c07..55542d7 100644
--- a/Source/com/drew/metadata/photoshop/PsdReader.java
+++ b/Source/com/drew/metadata/photoshop/PsdReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,10 +21,9 @@
 
 package com.drew.metadata.photoshop;
 
-import com.drew.lang.RandomAccessReader;
+import com.drew.lang.SequentialReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataReader;
 
 import java.io.IOException;
 
@@ -33,21 +32,24 @@ import java.io.IOException;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
-public class PsdReader implements MetadataReader
+public class PsdReader
 {
-    public void extract(@NotNull final RandomAccessReader reader, final @NotNull Metadata metadata)
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
     {
-        final PsdHeaderDirectory directory = metadata.getOrCreateDirectory(PsdHeaderDirectory.class);
+        PsdHeaderDirectory directory = new PsdHeaderDirectory();
+        metadata.addDirectory(directory);
+
+        // FILE HEADER SECTION
 
         try {
-            final int signature = reader.getInt32(0);
-            if (signature != 0x38425053)
+            final int signature = reader.getInt32();
+            if (signature != 0x38425053) // "8BPS"
             {
                 directory.addError("Invalid PSD file signature");
                 return;
             }
 
-            final int version = reader.getUInt16(4);
+            final int version = reader.getUInt16();
             if (version != 1 && version != 2)
             {
                 directory.addError("Invalid PSD file version (must be 1 or 2)");
@@ -55,25 +57,65 @@ public class PsdReader implements MetadataReader
             }
 
             // 6 reserved bytes are skipped here.  They should be zero.
+            reader.skip(6);
 
-            final int channelCount = reader.getUInt16(12);
+            final int channelCount = reader.getUInt16();
             directory.setInt(PsdHeaderDirectory.TAG_CHANNEL_COUNT, channelCount);
 
             // even though this is probably an unsigned int, the max height in practice is 300,000
-            final int imageHeight = reader.getInt32(14);
+            final int imageHeight = reader.getInt32();
             directory.setInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT, imageHeight);
 
             // even though this is probably an unsigned int, the max width in practice is 300,000
-            final int imageWidth = reader.getInt32(18);
+            final int imageWidth = reader.getInt32();
             directory.setInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH, imageWidth);
 
-            final int bitsPerChannel = reader.getUInt16(22);
+            final int bitsPerChannel = reader.getUInt16();
             directory.setInt(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL, bitsPerChannel);
 
-            final int colorMode = reader.getUInt16(24);
+            final int colorMode = reader.getUInt16();
             directory.setInt(PsdHeaderDirectory.TAG_COLOR_MODE, colorMode);
         } catch (IOException e) {
             directory.addError("Unable to read PSD header");
+            return;
         }
+
+        // COLOR MODE DATA SECTION
+
+        try {
+            long sectionLength = reader.getUInt32();
+
+            /*
+             * Only indexed color and duotone (see the mode field in the File header section) have color mode data.
+             * For all other modes, this section is just the 4-byte length field, which is set to zero.
+             *
+             * Indexed color images: length is 768; color data contains the color table for the image,
+             *                       in non-interleaved order.
+             * Duotone images: color data contains the duotone specification (the format of which is not documented).
+             *                 Other applications that read Photoshop files can treat a duotone image as a gray	image,
+             *                 and just preserve the contents of the duotone information when reading and writing the
+             *                 file.
+             */
+
+            reader.skip(sectionLength);
+        } catch (IOException e) {
+            return;
+        }
+
+        // IMAGE RESOURCES SECTION
+
+        try {
+            long sectionLength = reader.getUInt32();
+
+            assert(sectionLength <= Integer.MAX_VALUE);
+
+            new PhotoshopReader().extract(reader, (int)sectionLength, metadata);
+        } catch (IOException e) {
+            // ignore
+        }
+
+        // LAYER AND MASK INFORMATION SECTION (skipped)
+
+        // IMAGE DATA SECTION (skipped)
     }
 }
diff --git a/Source/com/drew/metadata/photoshop/package-info.java b/Source/com/drew/metadata/photoshop/package-info.java
new file mode 100644
index 0000000..90804c4
--- /dev/null
+++ b/Source/com/drew/metadata/photoshop/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of Photoshop metadata.
+ */
+package com.drew.metadata.photoshop;
diff --git a/Source/com/drew/metadata/photoshop/package.html b/Source/com/drew/metadata/photoshop/package.html
deleted file mode 100644
index 1bdf41e..0000000
--- a/Source/com/drew/metadata/photoshop/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of Photoshop metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java b/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java
index 77f9c3f..c64b88e 100644
--- a/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java
+++ b/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.png;
 
 import com.drew.lang.annotations.NotNull;
@@ -9,6 +29,7 @@ import java.util.HashMap;
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PngChromaticitiesDirectory extends Directory
 {
     public static final int TAG_WHITE_POINT_X = 1;
diff --git a/Source/com/drew/metadata/png/PngDescriptor.java b/Source/com/drew/metadata/png/PngDescriptor.java
index dd9d29f..b196987 100644
--- a/Source/com/drew/metadata/png/PngDescriptor.java
+++ b/Source/com/drew/metadata/png/PngDescriptor.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.png;
 
 import com.drew.imaging.png.PngColorType;
@@ -11,9 +31,12 @@ import com.drew.metadata.TagDescriptor;
 import java.io.IOException;
 import java.util.List;
 
+import static com.drew.metadata.png.PngDirectory.*;
+
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PngDescriptor extends TagDescriptor<PngDirectory>
 {
     public PngDescriptor(@NotNull PngDirectory directory)
@@ -26,22 +49,24 @@ public class PngDescriptor extends TagDescriptor<PngDirectory>
     public String getDescription(int tagType)
     {
         switch (tagType) {
-            case PngDirectory.TAG_COLOR_TYPE:
+            case TAG_COLOR_TYPE:
                 return getColorTypeDescription();
-            case PngDirectory.TAG_COMPRESSION_TYPE:
+            case TAG_COMPRESSION_TYPE:
                 return getCompressionTypeDescription();
-            case PngDirectory.TAG_FILTER_METHOD:
+            case TAG_FILTER_METHOD:
                 return getFilterMethodDescription();
-            case PngDirectory.TAG_INTERLACE_METHOD:
+            case TAG_INTERLACE_METHOD:
                 return getInterlaceMethodDescription();
-            case PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY:
+            case TAG_PALETTE_HAS_TRANSPARENCY:
                 return getPaletteHasTransparencyDescription();
-            case PngDirectory.TAG_SRGB_RENDERING_INTENT:
+            case TAG_SRGB_RENDERING_INTENT:
                 return getIsSrgbColorSpaceDescription();
-            case PngDirectory.TAG_TEXTUAL_DATA:
+            case TAG_TEXTUAL_DATA:
                 return getTextualDataDescription();
-            case PngDirectory.TAG_BACKGROUND_COLOR:
+            case TAG_BACKGROUND_COLOR:
                 return getBackgroundColorDescription();
+            case TAG_UNIT_SPECIFIER:
+                return getUnitSpecifierDescription();
             default:
                 return super.getDescription(tagType);
         }
@@ -50,7 +75,7 @@ public class PngDescriptor extends TagDescriptor<PngDirectory>
     @Nullable
     public String getColorTypeDescription()
     {
-        Integer value = _directory.getInteger(PngDirectory.TAG_COLOR_TYPE);
+        Integer value = _directory.getInteger(TAG_COLOR_TYPE);
         if (value == null)
             return null;
         PngColorType colorType = PngColorType.fromNumericValue(value);
@@ -62,32 +87,32 @@ public class PngDescriptor extends TagDescriptor<PngDirectory>
     @Nullable
     public String getCompressionTypeDescription()
     {
-        return getIndexedDescription(PngDirectory.TAG_COMPRESSION_TYPE, "Deflate");
+        return getIndexedDescription(TAG_COMPRESSION_TYPE, "Deflate");
     }
 
     @Nullable
     public String getFilterMethodDescription()
     {
-        return getIndexedDescription(PngDirectory.TAG_FILTER_METHOD, "Adaptive");
+        return getIndexedDescription(TAG_FILTER_METHOD, "Adaptive");
     }
 
     @Nullable
     public String getInterlaceMethodDescription()
     {
-        return getIndexedDescription(PngDirectory.TAG_INTERLACE_METHOD, "No Interlace", "Adam7 Interlace");
+        return getIndexedDescription(TAG_INTERLACE_METHOD, "No Interlace", "Adam7 Interlace");
     }
 
     @Nullable
     public String getPaletteHasTransparencyDescription()
     {
-        return getIndexedDescription(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, null, "Yes");
+        return getIndexedDescription(TAG_PALETTE_HAS_TRANSPARENCY, null, "Yes");
     }
 
     @Nullable
     public String getIsSrgbColorSpaceDescription()
     {
         return getIndexedDescription(
-            PngDirectory.TAG_SRGB_RENDERING_INTENT,
+            TAG_SRGB_RENDERING_INTENT,
             "Perceptual",
             "Relative Colorimetric",
             "Saturation",
@@ -95,10 +120,20 @@ public class PngDescriptor extends TagDescriptor<PngDirectory>
         );
     }
 
+    @Nullable
+    public String getUnitSpecifierDescription()
+    {
+        return getIndexedDescription(
+            TAG_UNIT_SPECIFIER,
+            "Unspecified",
+            "Metres"
+        );
+    }
+
     @Nullable
     public String getTextualDataDescription()
     {
-        Object object = _directory.getObject(PngDirectory.TAG_TEXTUAL_DATA);
+        Object object = _directory.getObject(TAG_TEXTUAL_DATA);
         if (object == null) {
             return null;
         }
@@ -106,7 +141,9 @@ public class PngDescriptor extends TagDescriptor<PngDirectory>
         List<KeyValuePair> keyValues = (List<KeyValuePair>)object;
         StringBuilder sb = new StringBuilder();
         for (KeyValuePair keyValue : keyValues) {
-            sb.append(String.format("%s: %s\n", keyValue.getKey(), keyValue.getValue()));
+            if (sb.length() != 0)
+                sb.append('\n');
+            sb.append(String.format("%s: %s", keyValue.getKey(), keyValue.getValue()));
         }
         return sb.toString();
     }
@@ -114,8 +151,8 @@ public class PngDescriptor extends TagDescriptor<PngDirectory>
     @Nullable
     public String getBackgroundColorDescription()
     {
-        byte[] bytes = _directory.getByteArray(PngDirectory.TAG_BACKGROUND_COLOR);
-        Integer colorType = _directory.getInteger(PngDirectory.TAG_COLOR_TYPE);
+        byte[] bytes = _directory.getByteArray(TAG_BACKGROUND_COLOR);
+        Integer colorType = _directory.getInteger(TAG_COLOR_TYPE);
         if (bytes == null || colorType == null) {
             return null;
         }
diff --git a/Source/com/drew/metadata/png/PngDirectory.java b/Source/com/drew/metadata/png/PngDirectory.java
index c10f2f3..1940630 100644
--- a/Source/com/drew/metadata/png/PngDirectory.java
+++ b/Source/com/drew/metadata/png/PngDirectory.java
@@ -1,5 +1,26 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.png;
 
+import com.drew.imaging.png.PngChunkType;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -8,6 +29,7 @@ import java.util.HashMap;
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class PngDirectory extends Directory
 {
     public static final int TAG_IMAGE_WIDTH = 1;
@@ -26,6 +48,12 @@ public class PngDirectory extends Directory
     public static final int TAG_LAST_MODIFICATION_TIME = 14;
     public static final int TAG_BACKGROUND_COLOR = 15;
 
+    public static final int TAG_PIXELS_PER_UNIT_X = 16;
+    public static final int TAG_PIXELS_PER_UNIT_Y = 17;
+    public static final int TAG_UNIT_SPECIFIER = 18;
+
+    public static final int TAG_SIGNIFICANT_BITS = 19;
+
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
@@ -45,18 +73,32 @@ public class PngDirectory extends Directory
         _tagNameMap.put(TAG_TEXTUAL_DATA, "Textual Data");
         _tagNameMap.put(TAG_LAST_MODIFICATION_TIME, "Last Modification Time");
         _tagNameMap.put(TAG_BACKGROUND_COLOR, "Background Color");
+        _tagNameMap.put(TAG_PIXELS_PER_UNIT_X, "Pixels Per Unit X");
+        _tagNameMap.put(TAG_PIXELS_PER_UNIT_Y, "Pixels Per Unit Y");
+        _tagNameMap.put(TAG_UNIT_SPECIFIER, "Unit Specifier");
+        _tagNameMap.put(TAG_SIGNIFICANT_BITS, "Significant Bits");
     }
 
-    public PngDirectory()
+    private final PngChunkType _pngChunkType;
+
+    public PngDirectory(@NotNull PngChunkType pngChunkType)
     {
+        _pngChunkType = pngChunkType;
+
         this.setDescriptor(new PngDescriptor(this));
     }
 
+    @NotNull
+    public PngChunkType getPngChunkType()
+    {
+        return _pngChunkType;
+    }
+
     @Override
     @NotNull
     public String getName()
     {
-        return "PNG";
+        return "PNG-" + _pngChunkType.getIdentifier();
     }
 
     @Override
diff --git a/Source/com/drew/metadata/png/package-info.java b/Source/com/drew/metadata/png/package-info.java
new file mode 100644
index 0000000..d427c0d
--- /dev/null
+++ b/Source/com/drew/metadata/png/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of PNG file metadata.
+ *
+ * @since 2.7.0
+ */
+package com.drew.metadata.png;
diff --git a/Source/com/drew/metadata/png/package.html b/Source/com/drew/metadata/png/package.html
deleted file mode 100644
index 20b9ba8..0000000
--- a/Source/com/drew/metadata/png/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of PNG file metadata.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java b/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java
index eb5e25a..163e86a 100644
--- a/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java
+++ b/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,7 +24,9 @@ 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.ErrorDirectory;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
 import java.util.Stack;
 
@@ -40,10 +42,9 @@ public abstract class DirectoryTiffHandler implements TiffHandler
     protected Directory _currentDirectory;
     protected final Metadata _metadata;
 
-    protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectory)
+    protected DirectoryTiffHandler(Metadata metadata)
     {
         _metadata = metadata;
-        _currentDirectory = _metadata.getOrCreateDirectory(initialDirectory);
     }
 
     public void endingIFD()
@@ -53,19 +54,49 @@ public abstract class DirectoryTiffHandler implements TiffHandler
 
     protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
     {
-        assert(directoryClass != _currentDirectory.getClass());
-        _directoryStack.push(_currentDirectory);
-        _currentDirectory = _metadata.getOrCreateDirectory(directoryClass);
+        Directory newDirectory = null;
+
+        try {
+            newDirectory = directoryClass.newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+
+        if (newDirectory != null)
+        {
+            // If this is the first directory, don't add to the stack
+            if (_currentDirectory != null)
+            {
+                _directoryStack.push(_currentDirectory);
+                newDirectory.setParent(_currentDirectory);
+            }
+            _currentDirectory = newDirectory;
+            _metadata.addDirectory(_currentDirectory);
+        }
     }
 
     public void warn(@NotNull String message)
     {
-        _currentDirectory.addError(message);
+        getCurrentOrErrorDirectory().addError(message);
     }
 
     public void error(@NotNull String message)
     {
-        _currentDirectory.addError(message);
+        getCurrentOrErrorDirectory().addError(message);
+    }
+
+    @NotNull
+    private Directory getCurrentOrErrorDirectory()
+    {
+        if (_currentDirectory != null)
+            return _currentDirectory;
+        ErrorDirectory error = _metadata.getFirstDirectoryOfType(ErrorDirectory.class);
+        if (error != null)
+            return error;
+        pushDirectory(ErrorDirectory.class);
+        return _currentDirectory;
     }
 
     public void setByteArray(int tagId, @NotNull byte[] bytes)
@@ -73,9 +104,9 @@ public abstract class DirectoryTiffHandler implements TiffHandler
         _currentDirectory.setByteArray(tagId, bytes);
     }
 
-    public void setString(int tagId, @NotNull String string)
+    public void setString(int tagId, @NotNull StringValue string)
     {
-        _currentDirectory.setString(tagId, string);
+        _currentDirectory.setStringValue(tagId, string);
     }
 
     public void setRational(int tagId, @NotNull Rational rational)
diff --git a/Source/com/drew/metadata/tiff/package-info.java b/Source/com/drew/metadata/tiff/package-info.java
new file mode 100644
index 0000000..28f57bb
--- /dev/null
+++ b/Source/com/drew/metadata/tiff/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of TIFF file metadata.
+ *
+ * @since 2.7.0
+ */
+package com.drew.metadata.tiff;
diff --git a/Source/com/drew/metadata/tiff/package.html b/Source/com/drew/metadata/tiff/package.html
deleted file mode 100644
index b0cc736..0000000
--- a/Source/com/drew/metadata/tiff/package.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of TIFF file metadata.
-
-<!-- Put @see and @since tags down here. -->
-@since 2.7.0
-
-</body>
-</html>
diff --git a/Source/com/drew/metadata/webp/WebpDescriptor.java b/Source/com/drew/metadata/webp/WebpDescriptor.java
new file mode 100644
index 0000000..7876f44
--- /dev/null
+++ b/Source/com/drew/metadata/webp/WebpDescriptor.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.webp;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class WebpDescriptor extends TagDescriptor<WebpDirectory>
+{
+    public WebpDescriptor(@NotNull WebpDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/webp/WebpDirectory.java b/Source/com/drew/metadata/webp/WebpDirectory.java
new file mode 100644
index 0000000..39ff922
--- /dev/null
+++ b/Source/com/drew/metadata/webp/WebpDirectory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.webp;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class WebpDirectory extends Directory
+{
+    public static final int TAG_IMAGE_HEIGHT = 1;
+    public static final int TAG_IMAGE_WIDTH = 2;
+    public static final int TAG_HAS_ALPHA = 3;
+    public static final int TAG_IS_ANIMATION = 4;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_HAS_ALPHA, "Has Alpha");
+        _tagNameMap.put(TAG_IS_ANIMATION, "Is Animation");
+    }
+
+    public WebpDirectory()
+    {
+        this.setDescriptor(new WebpDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "WebP";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
diff --git a/Source/com/drew/metadata/webp/WebpRiffHandler.java b/Source/com/drew/metadata/webp/WebpRiffHandler.java
new file mode 100644
index 0000000..208b39c
--- /dev/null
+++ b/Source/com/drew/metadata/webp/WebpRiffHandler.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.webp;
+
+import com.drew.imaging.riff.RiffHandler;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.exif.ExifReader;
+import com.drew.metadata.icc.IccReader;
+import com.drew.metadata.xmp.XmpReader;
+
+import java.io.IOException;
+
+/**
+ * Implementation of {@link RiffHandler} specialising in WebP support.
+ *
+ * Extracts data from chunk types:
+ *
+ * <ul>
+ *     <li><code>"VP8X"</code>: width, height, is animation, has alpha</li>
+ *     <li><code>"EXIF"</code>: full Exif data</li>
+ *     <li><code>"ICCP"</code>: full ICC profile</li>
+ *     <li><code>"XMP "</code>: full XMP data</li>
+ * </ul>
+ */
+public class WebpRiffHandler implements RiffHandler
+{
+    @NotNull
+    private final Metadata _metadata;
+
+    public WebpRiffHandler(@NotNull Metadata metadata)
+    {
+        _metadata = metadata;
+    }
+
+    public boolean shouldAcceptRiffIdentifier(@NotNull String identifier)
+    {
+        return identifier.equals("WEBP");
+    }
+
+    public boolean shouldAcceptChunk(@NotNull String fourCC)
+    {
+        return fourCC.equals("VP8X")
+            || fourCC.equals("VP8L")
+            || fourCC.equals("VP8 ")
+            || fourCC.equals("EXIF")
+            || fourCC.equals("ICCP")
+            || fourCC.equals("XMP ");
+    }
+
+    public void processChunk(@NotNull String fourCC, @NotNull byte[] payload)
+    {
+//        System.out.println("Chunk " + fourCC + " " + payload.length + " bytes");
+
+        if (fourCC.equals("EXIF")) {
+            new ExifReader().extract(new ByteArrayReader(payload), _metadata);
+        } else if (fourCC.equals("ICCP")) {
+            new IccReader().extract(new ByteArrayReader(payload), _metadata);
+        } else if (fourCC.equals("XMP ")) {
+            new XmpReader().extract(payload, _metadata);
+        } else if (fourCC.equals("VP8X") && payload.length == 10) {
+            RandomAccessReader reader = new ByteArrayReader(payload);
+            reader.setMotorolaByteOrder(false);
+
+            try {
+                // Flags
+//                boolean hasFragments = reader.getBit(0);
+                boolean isAnimation = reader.getBit(1);
+//                boolean hasXmp = reader.getBit(2);
+//                boolean hasExif = reader.getBit(3);
+                boolean hasAlpha = reader.getBit(4);
+//                boolean hasIcc = reader.getBit(5);
+
+                // Image size
+                int widthMinusOne = reader.getInt24(4);
+                int heightMinusOne = reader.getInt24(7);
+
+                WebpDirectory directory = new WebpDirectory();
+                directory.setInt(WebpDirectory.TAG_IMAGE_WIDTH, widthMinusOne + 1);
+                directory.setInt(WebpDirectory.TAG_IMAGE_HEIGHT, heightMinusOne + 1);
+                directory.setBoolean(WebpDirectory.TAG_HAS_ALPHA, hasAlpha);
+                directory.setBoolean(WebpDirectory.TAG_IS_ANIMATION, isAnimation);
+
+                _metadata.addDirectory(directory);
+
+            } catch (IOException e) {
+                e.printStackTrace(System.err);
+            }
+        } else if (fourCC.equals("VP8L") && payload.length > 4) {
+            RandomAccessReader reader = new ByteArrayReader(payload);
+            reader.setMotorolaByteOrder(false);
+
+            try {
+                // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#2_riff_header
+
+                // Expect the signature byte
+                if (reader.getInt8(0) != 0x2F)
+                    return;
+                int b1 = reader.getUInt8(1);
+                int b2 = reader.getUInt8(2);
+                int b3 = reader.getUInt8(3);
+                int b4 = reader.getUInt8(4);
+                // 14 bits for width
+                int widthMinusOne = (b2 & 0x3F) << 8 | b1;
+                // 14 bits for height
+                int heightMinusOne = (b4 & 0x0F) << 10 | b3 << 2 | (b2 & 0xC0) >> 6;
+
+                WebpDirectory directory = new WebpDirectory();
+                directory.setInt(WebpDirectory.TAG_IMAGE_WIDTH, widthMinusOne + 1);
+                directory.setInt(WebpDirectory.TAG_IMAGE_HEIGHT, heightMinusOne + 1);
+
+                _metadata.addDirectory(directory);
+
+            } catch (IOException e) {
+                e.printStackTrace(System.err);
+            }
+        } else if (fourCC.equals("VP8 ") && payload.length > 9) {
+            RandomAccessReader reader = new ByteArrayReader(payload);
+            reader.setMotorolaByteOrder(false);
+
+            try {
+                // https://tools.ietf.org/html/rfc6386#section-9.1
+                // https://github.com/webmproject/libwebp/blob/master/src/enc/syntax.c#L115
+
+                // Expect the signature bytes
+                if (reader.getUInt8(3) != 0x9D ||
+                    reader.getUInt8(4) != 0x01 ||
+                    reader.getUInt8(5) != 0x2A)
+                    return;
+                int width = reader.getUInt16(6);
+                int height = reader.getUInt16(8);
+
+                WebpDirectory directory = new WebpDirectory();
+                directory.setInt(WebpDirectory.TAG_IMAGE_WIDTH, width);
+                directory.setInt(WebpDirectory.TAG_IMAGE_HEIGHT, height);
+
+                _metadata.addDirectory(directory);
+
+            } catch (IOException e) {
+                e.printStackTrace(System.err);
+            }
+        }
+    }
+}
diff --git a/Source/com/drew/metadata/webp/package-info.java b/Source/com/drew/metadata/webp/package-info.java
new file mode 100644
index 0000000..26547ef
--- /dev/null
+++ b/Source/com/drew/metadata/webp/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains classes for the extraction and modelling of WebP file metadata.
+ *
+ * @since 2.8.0
+ */
+package com.drew.metadata.webp;
diff --git a/Source/com/drew/metadata/xmp/XmpDescriptor.java b/Source/com/drew/metadata/xmp/XmpDescriptor.java
index 5d9b88b..475f27f 100644
--- a/Source/com/drew/metadata/xmp/XmpDescriptor.java
+++ b/Source/com/drew/metadata/xmp/XmpDescriptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -20,156 +20,20 @@
  */
 package com.drew.metadata.xmp;
 
-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.TagDescriptor;
 
-import java.text.DecimalFormat;
-
 /**
  * Contains all logic for the presentation of xmp data, as stored in Xmp-Segment.  Use
  * this class to provide human-readable descriptions of tag values.
  *
  * @author Torsten Skadell, Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class XmpDescriptor extends TagDescriptor<XmpDirectory>
 {
-    // TODO some of these methods look similar to those found in Exif*Descriptor... extract common functionality from both
-
-    @NotNull
-    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
-
     public XmpDescriptor(@NotNull XmpDirectory directory)
     {
         super(directory);
     }
-
-    /** Do some simple formatting, dependant upon tagType */
-    @Override
-    public String getDescription(int tagType)
-    {
-        switch (tagType) {
-            case XmpDirectory.TAG_MAKE:
-            case XmpDirectory.TAG_MODEL:
-                return _directory.getString(tagType);
-            case XmpDirectory.TAG_EXPOSURE_TIME:
-                return getExposureTimeDescription();
-            case XmpDirectory.TAG_EXPOSURE_PROGRAM:
-                return getExposureProgramDescription();
-            case XmpDirectory.TAG_SHUTTER_SPEED:
-                return getShutterSpeedDescription();
-            case XmpDirectory.TAG_F_NUMBER:
-                return getFNumberDescription();
-            case XmpDirectory.TAG_LENS:
-            case XmpDirectory.TAG_LENS_INFO:
-            case XmpDirectory.TAG_CAMERA_SERIAL_NUMBER:
-            case XmpDirectory.TAG_FIRMWARE:
-                return _directory.getString(tagType);
-            case XmpDirectory.TAG_FOCAL_LENGTH:
-                return getFocalLengthDescription();
-            case XmpDirectory.TAG_APERTURE_VALUE:
-                return getApertureValueDescription();
-            default:
-                return super.getDescription(tagType);
-        }
-    }
-
-    /** Do a simple formatting like ExifSubIFDDescriptor.java */
-    @Nullable
-    public String getExposureTimeDescription()
-    {
-        final String value = _directory.getString(XmpDirectory.TAG_EXPOSURE_TIME);
-        if (value==null)
-            return null;
-        return value + " sec";
-    }
-
-    /** This code is from ExifSubIFDDescriptor.java */
-    @Nullable
-    public String getExposureProgramDescription()
-    {
-        // '1' means manual control, '2' program normal, '3' aperture priority,
-        // '4' shutter priority, '5' program creative (slow program),
-        // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode.
-        final Integer value = _directory.getInteger(XmpDirectory.TAG_EXPOSURE_PROGRAM);
-        if (value==null)
-            return null;
-        switch (value) {
-            case 1:
-                return "Manual control";
-            case 2:
-                return "Program normal";
-            case 3:
-                return "Aperture priority";
-            case 4:
-                return "Shutter priority";
-            case 5:
-                return "Program creative (slow program)";
-            case 6:
-                return "Program action (high-speed program)";
-            case 7:
-                return "Portrait mode";
-            case 8:
-                return "Landscape mode";
-            default:
-                return "Unknown program (" + value + ")";
-        }
-    }
-
-
-    /** This code is from ExifSubIFDDescriptor.java */
-    @Nullable
-    public String getShutterSpeedDescription()
-    {
-        final Float value = _directory.getFloatObject(XmpDirectory.TAG_SHUTTER_SPEED);
-        if (value==null)
-            return null;
-
-        // 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
-        if (value <= 1) {
-            float apexPower = (float) (1 / (Math.exp(value * 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(value * Math.log(2))));
-            return "1/" + apexPower + " sec";
-        }
-    }
-
-    /** Do a simple formatting like ExifSubIFDDescriptor.java */
-    @Nullable
-    public String getFNumberDescription()
-    {
-        final Rational value = _directory.getRational(XmpDirectory.TAG_F_NUMBER);
-        if (value==null)
-            return null;
-        return "F" + SimpleDecimalFormatter.format(value.doubleValue());
-    }
-
-    /** This code is from ExifSubIFDDescriptor.java */
-    @Nullable
-    public String getFocalLengthDescription()
-    {
-        final Rational value = _directory.getRational(XmpDirectory.TAG_FOCAL_LENGTH);
-        if (value==null)
-            return null;
-        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
-        return formatter.format(value.doubleValue()) + " mm";
-    }
-
-    /** This code is from ExifSubIFDDescriptor.java */
-    @Nullable
-    public String getApertureValueDescription()
-    {
-        final Double value = _directory.getDoubleObject(XmpDirectory.TAG_APERTURE_VALUE);
-        if (value==null)
-            return null;
-        double fStop = PhotographicConversions.apertureToFStop(value);
-        return "F" + SimpleDecimalFormatter.format(fStop);
-    }
 }
diff --git a/Source/com/drew/metadata/xmp/XmpDirectory.java b/Source/com/drew/metadata/xmp/XmpDirectory.java
index b261344..7f647d4 100644
--- a/Source/com/drew/metadata/xmp/XmpDirectory.java
+++ b/Source/com/drew/metadata/xmp/XmpDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -20,117 +20,39 @@
  */
 package com.drew.metadata.xmp;
 
+import com.adobe.xmp.XMPException;
 import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.impl.XMPMetaImpl;
+import com.adobe.xmp.properties.XMPPropertyInfo;
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 
 /**
+ * Wraps an instance of Adobe's {@link XMPMeta} object, which holds XMP data.
+ * <p />
+ * XMP uses a namespace and path format for identifying values, which does not map to metadata-extractor's
+ * integer based tag identifiers. Therefore, XMP data is extracted and exposed via {@link XmpDirectory#getXMPMeta()}
+ * which returns an instance of Adobe's {@link XMPMeta} which exposes the full XMP data set.
+ *
  * @author Torsten Skadell
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("WeakerAccess")
 public class XmpDirectory extends Directory
 {
-    // These are some Tags, belonging to xmp-data-tags
-    // The numeration is more like enums. The real xmp-tags are strings,
-    // so we do some kind of mapping here...
-    public static final int TAG_MAKE = 0x0001;
-    public static final int TAG_MODEL = 0x0002;
-    public static final int TAG_EXPOSURE_TIME = 0x0003;
-    public static final int TAG_SHUTTER_SPEED = 0x0004;
-    public static final int TAG_F_NUMBER = 0x0005;
-    public static final int TAG_LENS_INFO = 0x0006;
-    public static final int TAG_LENS = 0x0007;
-    public static final int TAG_CAMERA_SERIAL_NUMBER = 0x0008;
-    public static final int TAG_FIRMWARE = 0x0009;
-    public static final int TAG_FOCAL_LENGTH = 0x000a;
-    public static final int TAG_APERTURE_VALUE = 0x000b;
-    public static final int TAG_EXPOSURE_PROGRAM = 0x000c;
-    public static final int TAG_DATETIME_ORIGINAL = 0x000d;
-    public static final int TAG_DATETIME_DIGITIZED = 0x000e;
-
-    /**
-     * A value from 0 to 5, or -1 if the image is rejected.
-     */
-    public static final int TAG_RATING = 0x1001;
-
-/*
-    // dublin core properties
-    // this requires further research
-    public static int TAG_TITLE = 0x100;
-    public static int TAG_SUBJECT = 0x1001;
-    public static int TAG_DATE = 0x1002;
-    public static int TAG_TYPE = 0x1003;
-    public static int TAG_DESCRIPTION = 0x1004;
-    public static int TAG_RELATION = 0x1005;
-    public static int TAG_COVERAGE = 0x1006;
-    public static int TAG_CREATOR = 0x1007;
-    public static int TAG_PUBLISHER = 0x1008;
-    public static int TAG_CONTRIBUTOR = 0x1009;
-    public static int TAG_RIGHTS = 0x100A;
-    public static int TAG_FORMAT = 0x100B;
-    public static int TAG_IDENTIFIER = 0x100C;
-    public static int TAG_LANGUAGE = 0x100D;
-    public static int TAG_AUDIENCE = 0x100E;
-    public static int TAG_PROVENANCE = 0x100F;
-    public static int TAG_RIGHTS_HOLDER = 0x1010;
-    public static int TAG_INSTRUCTIONAL_METHOD = 0x1011;
-    public static int TAG_ACCRUAL_METHOD = 0x1012;
-    public static int TAG_ACCRUAL_PERIODICITY = 0x1013;
-    public static int TAG_ACCRUAL_POLICY = 0x1014;
-*/
+    public static final int TAG_XMP_VALUE_COUNT = 0xFFFF;
 
     @NotNull
     protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
-    @NotNull
-    private final Map<String, String> _propertyValueByPath = new HashMap<String, String>();
 
     static {
-        _tagNameMap.put(TAG_MAKE, "Make");
-        _tagNameMap.put(TAG_MODEL, "Model");
-        _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time");
-        _tagNameMap.put(TAG_SHUTTER_SPEED, "Shutter Speed Value");
-        _tagNameMap.put(TAG_F_NUMBER, "F-Number");
-        _tagNameMap.put(TAG_LENS_INFO, "Lens Information");
-        _tagNameMap.put(TAG_LENS, "Lens");
-        _tagNameMap.put(TAG_CAMERA_SERIAL_NUMBER, "Serial Number");
-        _tagNameMap.put(TAG_FIRMWARE, "Firmware");
-        _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length");
-        _tagNameMap.put(TAG_APERTURE_VALUE, "Aperture Value");
-        _tagNameMap.put(TAG_EXPOSURE_PROGRAM, "Exposure Program");
-        _tagNameMap.put(TAG_DATETIME_ORIGINAL, "Date/Time Original");
-        _tagNameMap.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized");
-
-        _tagNameMap.put(TAG_RATING, "Rating");
-
-/*
-        // this requires further research
-        _tagNameMap.put(TAG_TITLE, "Title");
-        _tagNameMap.put(TAG_SUBJECT, "Subject");
-        _tagNameMap.put(TAG_DATE, "Date");
-        _tagNameMap.put(TAG_TYPE, "Type");
-        _tagNameMap.put(TAG_DESCRIPTION, "Description");
-        _tagNameMap.put(TAG_RELATION, "Relation");
-        _tagNameMap.put(TAG_COVERAGE, "Coverage");
-        _tagNameMap.put(TAG_CREATOR, "Creator");
-        _tagNameMap.put(TAG_PUBLISHER, "Publisher");
-        _tagNameMap.put(TAG_CONTRIBUTOR, "Contributor");
-        _tagNameMap.put(TAG_RIGHTS, "Rights");
-        _tagNameMap.put(TAG_FORMAT, "Format");
-        _tagNameMap.put(TAG_IDENTIFIER, "Identifier");
-        _tagNameMap.put(TAG_LANGUAGE, "Language");
-        _tagNameMap.put(TAG_AUDIENCE, "Audience");
-        _tagNameMap.put(TAG_PROVENANCE, "Provenance");
-        _tagNameMap.put(TAG_RIGHTS_HOLDER, "Rights Holder");
-        _tagNameMap.put(TAG_INSTRUCTIONAL_METHOD, "Instructional Method");
-        _tagNameMap.put(TAG_ACCRUAL_METHOD, "Accrual Method");
-        _tagNameMap.put(TAG_ACCRUAL_PERIODICITY, "Accrual Periodicity");
-        _tagNameMap.put(TAG_ACCRUAL_POLICY, "Accrual Policy");
-*/
+        _tagNameMap.put(TAG_XMP_VALUE_COUNT, "XMP Value Count");
     }
 
     @Nullable
@@ -145,7 +67,7 @@ public class XmpDirectory extends Directory
     @NotNull
     public String getName()
     {
-        return "Xmp";
+        return "XMP";
     }
 
     @Override
@@ -155,13 +77,8 @@ public class XmpDirectory extends Directory
         return _tagNameMap;
     }
 
-    void addProperty(@NotNull String path, @NotNull String value)
-    {
-        _propertyValueByPath.put(path, value);
-    }
-
     /**
-     * Gets a map of all XMP properties in this directory, not just the known ones.
+     * Gets a map of all XMP properties in this directory.
      * <p>
      * This is required because XMP properties are represented as strings, whereas the rest of this library
      * uses integers for keys.
@@ -169,20 +86,52 @@ public class XmpDirectory extends Directory
     @NotNull
     public Map<String, String> getXmpProperties()
     {
-        return Collections.unmodifiableMap(_propertyValueByPath);
+        Map<String, String> propertyValueByPath = new HashMap<String, String>();
+
+        if (_xmpMeta != null)
+        {
+            try {
+                for (Iterator i = _xmpMeta.iterator(); i.hasNext(); ) {
+                    XMPPropertyInfo prop = (XMPPropertyInfo)i.next();
+                    String path = prop.getPath();
+                    String value = prop.getValue();
+                    if (path != null && value != null) {
+                        propertyValueByPath.put(path, value);
+                    }
+                }
+            } catch (XMPException ignored) {
+            }
+        }
+
+        return Collections.unmodifiableMap(propertyValueByPath);
     }
 
     public void setXMPMeta(@NotNull XMPMeta xmpMeta)
     {
         _xmpMeta = xmpMeta;
+
+        try {
+            int valueCount = 0;
+            for (Iterator i = _xmpMeta.iterator(); i.hasNext(); ) {
+                XMPPropertyInfo prop = (XMPPropertyInfo)i.next();
+                if (prop.getPath() != null) {
+                    valueCount++;
+                }
+            }
+            setInt(TAG_XMP_VALUE_COUNT, valueCount);
+        } catch (XMPException ignored) {
+        }
     }
 
     /**
-     * Gets the XMPMeta object used to populate this directory.  It can be used for more XMP-oriented operations.
+     * Gets the XMPMeta object used to populate this directory. It can be used for more XMP-oriented operations.
+     * If one does not exist it will be created.
      */
-    @Nullable
+    @NotNull
     public XMPMeta getXMPMeta()
     {
+        if (_xmpMeta == null)
+            _xmpMeta = new XMPMetaImpl();
         return _xmpMeta;
     }
 }
diff --git a/Source/com/drew/metadata/xmp/XmpReader.java b/Source/com/drew/metadata/xmp/XmpReader.java
index 4c9adcc..bfcb2bb 100644
--- a/Source/com/drew/metadata/xmp/XmpReader.java
+++ b/Source/com/drew/metadata/xmp/XmpReader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,88 +24,105 @@ import com.adobe.xmp.XMPException;
 import com.adobe.xmp.XMPIterator;
 import com.adobe.xmp.XMPMeta;
 import com.adobe.xmp.XMPMetaFactory;
+import com.adobe.xmp.impl.ByteBuffer;
 import com.adobe.xmp.properties.XMPPropertyInfo;
 import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
 import com.drew.imaging.jpeg.JpegSegmentType;
-import com.drew.lang.Rational;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.metadata.Directory;
 import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Metadata;
+import com.drew.metadata.StringValue;
 
-import java.util.Arrays;
-import java.util.Calendar;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
 
 /**
- * Extracts XMP data from a JPEG header segment.
+ * Extracts XMP data from JPEG APP1 segments.
+ * <p>
+ * Note that XMP uses a namespace and path format for identifying values, which does not map to metadata-extractor's
+ * integer based tag identifiers. Therefore, XMP data is extracted and exposed via {@link XmpDirectory#getXMPMeta()}
+ * which returns an instance of Adobe's {@link XMPMeta} which exposes the full XMP data set.
  * <p>
  * The extraction is done with Adobe's XmpCore-Library (XMP-Toolkit)
  * Copyright (c) 1999 - 2007, Adobe Systems Incorporated All rights reserved.
  *
  * @author Torsten Skadell
  * @author Drew Noakes https://drewnoakes.com
+ * @author https://github.com/bezineb5
  */
 public class XmpReader implements JpegSegmentMetadataReader
 {
-    private static final int FMT_STRING = 1;
-    private static final int FMT_RATIONAL = 2;
-    private static final int FMT_INT = 3;
-    private static final int FMT_DOUBLE = 4;
-
-    /**
-     * XMP tag namespace.
-     * TODO the older "xap", "xapBJ", "xapMM" or "xapRights" namespace prefixes should be translated to the newer "xmp", "xmpBJ", "xmpMM" and "xmpRights" prefixes for use in family 1 group names
-     */
     @NotNull
-    private static final String SCHEMA_XMP_PROPERTIES = "http://ns.adobe.com/xap/1.0/";
+    private static final String XMP_JPEG_PREAMBLE = "http://ns.adobe.com/xap/1.0/\0";
     @NotNull
-    private static final String SCHEMA_EXIF_SPECIFIC_PROPERTIES = "http://ns.adobe.com/exif/1.0/";
+    private static final String XMP_EXTENSION_JPEG_PREAMBLE = "http://ns.adobe.com/xmp/extension/\0";
     @NotNull
-    private static final String SCHEMA_EXIF_ADDITIONAL_PROPERTIES = "http://ns.adobe.com/exif/1.0/aux/";
+    private static final String SCHEMA_XMP_NOTES = "http://ns.adobe.com/xmp/note/";
     @NotNull
-    private static final String SCHEMA_EXIF_TIFF_PROPERTIES = "http://ns.adobe.com/tiff/1.0/";
-//    @NotNull
-//    private static final String SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES = "http://purl.org/dc/elements/1.1/";
+    private static final String ATTRIBUTE_EXTENDED_XMP = "xmpNote:HasExtendedXMP";
+
+    /**
+     * Extended XMP constants
+     */
+    private static final int EXTENDED_XMP_GUID_LENGTH = 32;
+    private static final int EXTENDED_XMP_INT_LENGTH = 4;
 
     @NotNull
     public Iterable<JpegSegmentType> getSegmentTypes()
     {
-        return Arrays.asList(JpegSegmentType.APP1);
-    }
-
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
-    {
-        return segmentBytes.length > 27 && "http://ns.adobe.com/xap/1.0/".equalsIgnoreCase(new String(segmentBytes, 0, 28));
+        return Collections.singletonList(JpegSegmentType.APP1);
     }
 
     /**
      * Version specifically for dealing with XMP found in JPEG segments. This form of XMP has a peculiar preamble, which
      * must be removed before parsing the XML.
      *
-     * @param segmentBytes The byte array from which the metadata should be extracted.
+     * @param segments The byte array from which the metadata should be extracted.
      * @param metadata The {@link Metadata} object into which extracted values should be merged.
      * @param segmentType The {@link JpegSegmentType} being read.
      */
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
     {
-        XmpDirectory directory = metadata.getOrCreateDirectory(XmpDirectory.class);
+        final int preambleLength = XMP_JPEG_PREAMBLE.length();
+        final int extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length();
+        String extendedXMPGUID = null;
+        byte[] extendedXMPBuffer = null;
 
-        // XMP in a JPEG file has a 29 byte preamble which is not valid XML.
-        final int preambleLength = 29;
+        for (byte[] segmentBytes : segments) {
+            // XMP in a JPEG file has an identifying preamble which is not valid XML
+            if (segmentBytes.length >= preambleLength) {
+                // NOTE we expect the full preamble here, but some images (such as that reported on GitHub #102)
+                // start with "XMP\0://ns.adobe.com/xap/1.0/" which appears to be an error but is easily recovered
+                // from. In such cases, the actual XMP data begins at the same offset.
+                if (XMP_JPEG_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength)) ||
+                    "XMP".equalsIgnoreCase(new String(segmentBytes, 0, 3))) {
 
-        // check for the header length
-        if (segmentBytes.length <= preambleLength + 1) {
-            directory.addError(String.format("Xmp data segment must contain at least %d bytes", preambleLength + 1));
-            return;
-        }
+                    byte[] xmlBytes = new byte[segmentBytes.length - preambleLength];
+                    System.arraycopy(segmentBytes, preambleLength, xmlBytes, 0, xmlBytes.length);
+                    extract(xmlBytes, metadata);
+                    // Check in the Standard XMP if there should be a Extended XMP part in other chunks.
+                    extendedXMPGUID = getExtendedXMPGUID(metadata);
+                    continue;
+                }
+            }
 
-        String preamble = new String(segmentBytes, 0, preambleLength);
-        if (!"http://ns.adobe.com/xap/1.0/\0".equals(preamble)) {
-            directory.addError("XMP data segment doesn't begin with 'http://ns.adobe.com/xap/1.0/'");
-            return;
+            // If we know that there's Extended XMP chunks, look for them.
+            if (extendedXMPGUID != null &&
+                segmentBytes.length >= extensionPreambleLength &&
+                XMP_EXTENSION_JPEG_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, extensionPreambleLength))) {
+
+                extendedXMPBuffer = processExtendedXMPChunk(metadata, segmentBytes, extendedXMPGUID, extendedXMPBuffer);
+            }
         }
 
-        byte[] xmlBytes = new byte[segmentBytes.length - preambleLength];
-        System.arraycopy(segmentBytes, 29, xmlBytes, 0, xmlBytes.length);
-        extract(xmlBytes, metadata);
+        // Now that the Extended XMP chunks have been concatenated, let's parse and merge with the Standard XMP.
+        if (extendedXMPBuffer != null) {
+            extract(extendedXMPBuffer, metadata);
+        }
     }
 
     /**
@@ -115,14 +132,49 @@ public class XmpReader implements JpegSegmentMetadataReader
      */
     public void extract(@NotNull final byte[] xmpBytes, @NotNull Metadata metadata)
     {
-        XmpDirectory directory = metadata.getOrCreateDirectory(XmpDirectory.class);
+        extract(xmpBytes, metadata, null);
+    }
+
+    /**
+     * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}.
+     * <p>
+     * The extraction is done with Adobe's XMPCore library.
+     */
+    public void extract(@NotNull final byte[] xmpBytes, @NotNull Metadata metadata, @Nullable Directory parentDirectory)
+    {
+        extract(xmpBytes, 0, xmpBytes.length, metadata, parentDirectory);
+    }
+
+    /**
+     * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}.
+     * <p>
+     * The extraction is done with Adobe's XMPCore library.
+     */
+    public void extract(@NotNull final byte[] xmpBytes, int offset, int length, @NotNull Metadata metadata, @Nullable Directory parentDirectory)
+    {
+        XmpDirectory directory = new XmpDirectory();
+
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
 
         try {
-            XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes);
-            processXmpTags(directory, xmpMeta);
+            XMPMeta xmpMeta;
+
+            // If all xmpBytes are requested, no need to make a new ByteBuffer
+            if (offset == 0 && length == xmpBytes.length) {
+                xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes);
+            } else {
+                ByteBuffer buffer = new ByteBuffer(xmpBytes, offset, length);
+                xmpMeta = XMPMetaFactory.parse(buffer.getByteStream());
+            }
+
+            directory.setXMPMeta(xmpMeta);
         } catch (XMPException e) {
             directory.addError("Error processing XMP data: " + e.getMessage());
         }
+
+        if (!directory.isEmpty())
+            metadata.addDirectory(directory);
     }
 
     /**
@@ -132,130 +184,124 @@ public class XmpReader implements JpegSegmentMetadataReader
      */
     public void extract(@NotNull final String xmpString, @NotNull Metadata metadata)
     {
-        XmpDirectory directory = metadata.getOrCreateDirectory(XmpDirectory.class);
+        extract(xmpString, metadata, null);
+    }
+
+    /**
+     * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}.
+     * <p>
+     * The extraction is done with Adobe's XMPCore library.
+     */
+    public void extract(@NotNull final StringValue xmpString, @NotNull Metadata metadata)
+    {
+        extract(xmpString.getBytes(), metadata, null);
+    }
+
+    /**
+     * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}.
+     * <p>
+     * The extraction is done with Adobe's XMPCore library.
+     */
+    public void extract(@NotNull final String xmpString, @NotNull Metadata metadata, @Nullable Directory parentDirectory)
+    {
+        XmpDirectory directory = new XmpDirectory();
+
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
 
         try {
             XMPMeta xmpMeta = XMPMetaFactory.parseFromString(xmpString);
-            processXmpTags(directory, xmpMeta);
+            directory.setXMPMeta(xmpMeta);
         } catch (XMPException e) {
             directory.addError("Error processing XMP data: " + e.getMessage());
         }
+
+        if (!directory.isEmpty())
+            metadata.addDirectory(directory);
     }
 
-    private static void processXmpTags(XmpDirectory directory, XMPMeta xmpMeta) throws XMPException
+    /**
+     * Determine if there is an extended XMP section based on the standard XMP part.
+     * The xmpNote:HasExtendedXMP attribute contains the GUID of the Extended XMP chunks.
+     */
+    @Nullable
+    private static String getExtendedXMPGUID(@NotNull Metadata metadata)
     {
-        // store the XMPMeta object on the directory in case others wish to use it
-        directory.setXMPMeta(xmpMeta);
+        final Collection<XmpDirectory> xmpDirectories = metadata.getDirectoriesOfType(XmpDirectory.class);
 
-        // read all the tags and send them to the directory
-        // I've added some popular tags, feel free to add more tags
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:LensInfo", XmpDirectory.TAG_LENS_INFO, FMT_STRING);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:Lens", XmpDirectory.TAG_LENS, FMT_STRING);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:SerialNumber", XmpDirectory.TAG_CAMERA_SERIAL_NUMBER, FMT_STRING);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:Firmware", XmpDirectory.TAG_FIRMWARE, FMT_STRING);
+        for (XmpDirectory directory : xmpDirectories) {
+            final XMPMeta xmpMeta = directory.getXMPMeta();
 
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_TIFF_PROPERTIES, "tiff:Make", XmpDirectory.TAG_MAKE, FMT_STRING);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_TIFF_PROPERTIES, "tiff:Model", XmpDirectory.TAG_MODEL, FMT_STRING);
+            try {
+                final XMPIterator itr = xmpMeta.iterator(SCHEMA_XMP_NOTES, null, null);
+                if (itr == null)
+                    continue;
 
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ExposureTime", XmpDirectory.TAG_EXPOSURE_TIME, FMT_STRING);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ExposureProgram", XmpDirectory.TAG_EXPOSURE_PROGRAM, FMT_INT);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ApertureValue", XmpDirectory.TAG_APERTURE_VALUE, FMT_RATIONAL);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:FNumber", XmpDirectory.TAG_F_NUMBER, FMT_RATIONAL);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:FocalLength", XmpDirectory.TAG_FOCAL_LENGTH, FMT_RATIONAL);
-        processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ShutterSpeedValue", XmpDirectory.TAG_SHUTTER_SPEED, FMT_RATIONAL);
-
-        processXmpDateTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:DateTimeOriginal", XmpDirectory.TAG_DATETIME_ORIGINAL);
-        processXmpDateTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:DateTimeDigitized", XmpDirectory.TAG_DATETIME_DIGITIZED);
-
-        processXmpTag(xmpMeta, directory, SCHEMA_XMP_PROPERTIES, "xmp:Rating", XmpDirectory.TAG_RATING, FMT_DOUBLE);
-
-/*
-            // this requires further research
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:title", XmpDirectory.TAG_TITLE, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:subject", XmpDirectory.TAG_SUBJECT, FMT_STRING);
-            processXmpDateTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:date", XmpDirectory.TAG_DATE);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:type", XmpDirectory.TAG_TYPE, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:description", XmpDirectory.TAG_DESCRIPTION, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:relation", XmpDirectory.TAG_RELATION, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:coverage", XmpDirectory.TAG_COVERAGE, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:creator", XmpDirectory.TAG_CREATOR, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:publisher", XmpDirectory.TAG_PUBLISHER, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:contributor", XmpDirectory.TAG_CONTRIBUTOR, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:rights", XmpDirectory.TAG_RIGHTS, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:format", XmpDirectory.TAG_FORMAT, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:identifier", XmpDirectory.TAG_IDENTIFIER, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:language", XmpDirectory.TAG_LANGUAGE, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:audience", XmpDirectory.TAG_AUDIENCE, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:provenance", XmpDirectory.TAG_PROVENANCE, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:rightsHolder", XmpDirectory.TAG_RIGHTS_HOLDER, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:instructionalMethod", XmpDirectory.TAG_INSTRUCTIONAL_METHOD, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:accrualMethod", XmpDirectory.TAG_ACCRUAL_METHOD, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:accrualPeriodicity", XmpDirectory.TAG_ACCRUAL_PERIODICITY, FMT_STRING);
-            processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:accrualPolicy", XmpDirectory.TAG_ACCRUAL_POLICY, FMT_STRING);
-*/
-
-        for (XMPIterator iterator = xmpMeta.iterator(); iterator.hasNext(); ) {
-            XMPPropertyInfo propInfo = (XMPPropertyInfo) iterator.next();
-            String path = propInfo.getPath();
-            String value = propInfo.getValue();
-            if (path != null && value != null)
-                directory.addProperty(path, value);
+                while (itr.hasNext()) {
+                    final XMPPropertyInfo pi = (XMPPropertyInfo) itr.next();
+                    if (ATTRIBUTE_EXTENDED_XMP.equals(pi.getPath())) {
+                        return pi.getValue();
+                    }
+                }
+            } catch (XMPException e) {
+                // Fail silently here: we had a reading issue, not a decoding issue.
+            }
         }
+
+        return null;
     }
 
     /**
-     * Reads an property value with given namespace URI and property name. Add property value to directory if exists
+     * Process an Extended XMP chunk. It will read the bytes from segmentBytes and validates that the GUID the requested one.
+     * It will progressively fill the buffer with each chunk.
+     * The format is specified in this document:
+     * http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
+     * at page 19
      */
-    private static void processXmpTag(@NotNull XMPMeta meta, @NotNull XmpDirectory directory, @NotNull String schemaNS, @NotNull String propName, int tagType, int formatCode) throws XMPException
+    @Nullable
+    private static byte[] processExtendedXMPChunk(@NotNull Metadata metadata, @NotNull byte[] segmentBytes, @NotNull String extendedXMPGUID, @Nullable byte[] extendedXMPBuffer)
     {
-        String property = meta.getPropertyString(schemaNS, propName);
-
-        if (property == null)
-            return;
-
-        switch (formatCode) {
-            case FMT_RATIONAL:
-                String[] rationalParts = property.split("/", 2);
-                if (rationalParts.length == 2) {
-                    try {
-                        Rational rational = new Rational((long) Float.parseFloat(rationalParts[0]), (long) Float.parseFloat(rationalParts[1]));
-                        directory.setRational(tagType, rational);
-                    } catch (NumberFormatException ex) {
-                        directory.addError(String.format("Unable to parse XMP property %s as a Rational.", propName));
+        final int extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length();
+        final int segmentLength = segmentBytes.length;
+        final int totalOffset = extensionPreambleLength + EXTENDED_XMP_GUID_LENGTH + EXTENDED_XMP_INT_LENGTH + EXTENDED_XMP_INT_LENGTH;
+
+        if (segmentLength >= totalOffset) {
+            try {
+                /*
+                 * The chunk contains:
+                 * - A null-terminated signature string of "http://ns.adobe.com/xmp/extension/".
+                 * - A 128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination.
+                 *   The GUID is a 128-bit MD5 digest of the full ExtendedXMP serialization.
+                 * - The full length of the ExtendedXMP serialization as a 32-bit unsigned integer
+                 * - The offset of this portion as a 32-bit unsigned integer
+                 * - The portion of the ExtendedXMP
+                 */
+                final SequentialReader reader = new SequentialByteArrayReader(segmentBytes);
+                reader.skip(extensionPreambleLength);
+                final String segmentGUID = reader.getString(EXTENDED_XMP_GUID_LENGTH);
+
+                if (extendedXMPGUID.equals(segmentGUID)) {
+                    final int fullLength = (int)reader.getUInt32();
+                    final int chunkOffset = (int)reader.getUInt32();
+
+                    if (extendedXMPBuffer == null)
+                        extendedXMPBuffer = new byte[fullLength];
+
+                    if (extendedXMPBuffer.length == fullLength) {
+                        System.arraycopy(segmentBytes, totalOffset, extendedXMPBuffer, chunkOffset, segmentLength - totalOffset);
+                    } else {
+                        XmpDirectory directory = new XmpDirectory();
+                        directory.addError(String.format("Inconsistent length for the Extended XMP buffer: %d instead of %d", fullLength, extendedXMPBuffer.length));
+                        metadata.addDirectory(directory);
                     }
-                } else {
-                    directory.addError("Error in rational format for tag " + tagType);
-                }
-                break;
-            case FMT_INT:
-                try {
-                    directory.setInt(tagType, Integer.valueOf(property));
-                } catch (NumberFormatException ex) {
-                    directory.addError(String.format("Unable to parse XMP property %s as an int.", propName));
-                }
-                break;
-            case FMT_DOUBLE:
-                try {
-                    directory.setDouble(tagType, Double.valueOf(property));
-                } catch (NumberFormatException ex) {
-                    directory.addError(String.format("Unable to parse XMP property %s as an double.", propName));
                 }
-                break;
-            case FMT_STRING:
-                directory.setString(tagType, property);
-                break;
-            default:
-                directory.addError(String.format("Unknown format code %d for tag %d", formatCode, tagType));
+            } catch (IOException ex) {
+                XmpDirectory directory = new XmpDirectory();
+                directory.addError(ex.getMessage());
+                metadata.addDirectory(directory);
+            }
         }
-    }
-
-    @SuppressWarnings({"SameParameterValue"})
-    private static void processXmpDateTag(@NotNull XMPMeta meta, @NotNull XmpDirectory directory, @NotNull String schemaNS, @NotNull String propName, int tagType) throws XMPException
-    {
-        Calendar cal = meta.getPropertyCalendar(schemaNS, propName);
 
-        if (cal != null) {
-            directory.setDate(tagType, cal.getTime());
-        }
+        return extendedXMPBuffer;
     }
 }
diff --git a/Source/com/drew/metadata/xmp/XmpWriter.java b/Source/com/drew/metadata/xmp/XmpWriter.java
new file mode 100644
index 0000000..462b076
--- /dev/null
+++ b/Source/com/drew/metadata/xmp/XmpWriter.java
@@ -0,0 +1,37 @@
+package com.drew.metadata.xmp;
+
+import java.io.OutputStream;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.XMPMetaFactory;
+import com.adobe.xmp.options.SerializeOptions;
+import com.drew.metadata.Metadata;
+
+public class XmpWriter
+{
+    /**
+     * Serializes the XmpDirectory component of <code>Metadata</code> into an <code>OutputStream</code>
+     * @param os Destination for the xmp data
+     * @param data populated metadata
+     * @return serialize success
+     */
+    public static boolean write(OutputStream os, Metadata data)
+    {
+        XmpDirectory dir = data.getFirstDirectoryOfType(XmpDirectory.class);
+        if (dir == null)
+            return false;
+        XMPMeta meta = dir.getXMPMeta();
+        try
+        {
+            SerializeOptions so = new SerializeOptions().setOmitPacketWrapper(true);
+            XMPMetaFactory.serialize(meta, os, so);
+        }
+        catch (XMPException e)
+        {
+            e.printStackTrace();
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/Source/com/drew/metadata/xmp/package-info.java b/Source/com/drew/metadata/xmp/package-info.java
new file mode 100644
index 0000000..38eb9fe
--- /dev/null
+++ b/Source/com/drew/metadata/xmp/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of Adobe's XMP metadata.
+ */
+package com.drew.metadata.xmp;
diff --git a/Source/com/drew/metadata/xmp/package.html b/Source/com/drew/metadata/xmp/package.html
deleted file mode 100644
index f62e225..0000000
--- a/Source/com/drew/metadata/xmp/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes for the extraction and modelling of Adobe's XMP metadata.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Source/com/drew/tools/ExtractJpegSegmentTool.java b/Source/com/drew/tools/ExtractJpegSegmentTool.java
index f02a550..e62c876 100644
--- a/Source/com/drew/tools/ExtractJpegSegmentTool.java
+++ b/Source/com/drew/tools/ExtractJpegSegmentTool.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@ import java.util.Set;
 
 /**
  * Extracts JPEG segments and writes them to individual files.
- * <p/>
+ * <p>
  * Extracting only the required segment(s) for use in unit testing has several benefits:
  * <ul>
  *     <li>Helps reduce the repository size. For example a small JPEG image may still be 20kB+ in size, yet its
diff --git a/Source/com/drew/tools/FileUtil.java b/Source/com/drew/tools/FileUtil.java
index 6ea94fb..3c8deba 100644
--- a/Source/com/drew/tools/FileUtil.java
+++ b/Source/com/drew/tools/FileUtil.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java b/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java
index 06fe97a..e2fd26e 100644
--- a/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java
+++ b/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,8 +21,13 @@
 
 package com.drew.tools;
 
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPIterator;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.properties.XMPPropertyInfo;
+import com.drew.imaging.FileType;
+import com.drew.imaging.FileTypeDetector;
 import com.drew.imaging.ImageMetadataReader;
-import com.drew.imaging.ImageProcessingException;
 import com.drew.imaging.jpeg.JpegProcessingException;
 import com.drew.lang.StringUtil;
 import com.drew.lang.annotations.NotNull;
@@ -33,6 +38,8 @@ import com.drew.metadata.Tag;
 import com.drew.metadata.exif.ExifIFD0Directory;
 import com.drew.metadata.exif.ExifSubIFDDirectory;
 import com.drew.metadata.exif.ExifThumbnailDirectory;
+import com.drew.metadata.file.FileMetadataDirectory;
+import com.drew.metadata.xmp.XmpDirectory;
 
 import java.io.*;
 import java.util.*;
@@ -44,48 +51,70 @@ public class ProcessAllImagesInFolderUtility
 {
     public static void main(String[] args) throws IOException, JpegProcessingException
     {
-        if (args.length == 0) {
-            System.err.println("Expects one or more directories as arguments.");
-            System.exit(1);
-        }
-
         List<String> directories = new ArrayList<String>();
 
         FileHandler handler = null;
+        PrintStream log = System.out;
 
-        for (String arg : args) {
-            if (arg.equalsIgnoreCase("-text")) {
-                // If "-text" is specified, write the discovered metadata into a sub-folder relative to the image
+        for (int i = 0; i < args.length; i++) {
+            String arg = args[i];
+            if (arg.equalsIgnoreCase("--text")) {
+                // If "--text" is specified, write the discovered metadata into a sub-folder relative to the image
                 handler = new TextFileOutputHandler();
-            } else if (arg.equalsIgnoreCase("-markdown")) {
-                // If "-markdown" is specified, write a summary table in markdown format to standard out
+            } else if (arg.equalsIgnoreCase("--markdown")) {
+                // If "--markdown" is specified, write a summary table in markdown format to standard out
                 handler = new MarkdownTableOutputHandler();
+            } else if (arg.equalsIgnoreCase("--unknown")) {
+                // If "--unknown" is specified, write CSV tallying unknown tag counts
+                handler = new UnknownTagHandler();
+            } else if (arg.equalsIgnoreCase("--log-file")) {
+                if (i == args.length - 1) {
+                    printUsage();
+                    System.exit(1);
+                }
+                log = new PrintStream(new FileOutputStream(args[++i], false), true);
             } else {
                 // Treat this argument as a directory
                 directories.add(arg);
             }
         }
 
+        if (directories.isEmpty()) {
+            System.err.println("Expects one or more directories as arguments.");
+            printUsage();
+            System.exit(1);
+        }
+
         if (handler == null) {
             handler = new BasicFileHandler();
         }
 
         long start = System.nanoTime();
 
-        // Order alphabetically so that output is stable across invocations
-        Collections.sort(directories);
-
         for (String directory : directories) {
-            processDirectory(new File(directory), handler, "");
+            processDirectory(new File(directory), handler, "", log);
         }
 
-        handler.onCompleted();
+        handler.onScanCompleted(log);
 
         System.out.println(String.format("Completed in %d ms", (System.nanoTime() - start) / 1000000));
+
+        if (log != System.out) {
+            log.close();
+        }
+    }
+
+    private static void printUsage()
+    {
+        System.out.println("Usage:");
+        System.out.println();
+        System.out.println("  java com.drew.tools.ProcessAllImagesInFolderUtility [--text|--markdown|--unknown] [--log-file <file-name>]");
     }
 
-    private static void processDirectory(@NotNull File path, @NotNull FileHandler handler, @NotNull String relativePath)
+    private static void processDirectory(@NotNull File path, @NotNull FileHandler handler, @NotNull String relativePath, PrintStream log)
     {
+        handler.onStartingDirectory(path);
+
         String[] pathItems = path.list();
 
         if (pathItems == null) {
@@ -99,40 +128,51 @@ public class ProcessAllImagesInFolderUtility
             File file = new File(path, pathItem);
 
             if (file.isDirectory()) {
-                processDirectory(file, handler, relativePath.length() == 0 ? pathItem : relativePath + "/" + pathItem);
+                processDirectory(file, handler, relativePath.length() == 0 ? pathItem : relativePath + "/" + pathItem, log);
             } else if (handler.shouldProcess(file)) {
 
-                handler.onProcessingStarting(file);
+                handler.onBeforeExtraction(file, log, relativePath);
 
                 // Read metadata
                 final Metadata metadata;
                 try {
                     metadata = ImageMetadataReader.readMetadata(file);
                 } catch (Throwable t) {
-                    handler.onException(file, t);
+                    handler.onExtractionError(file, t, log);
                     continue;
                 }
 
-                handler.onExtracted(file, metadata, relativePath);
+                handler.onExtractionSuccess(file, metadata, relativePath, log);
             }
         }
     }
 
     interface FileHandler
     {
+        /** Called when the scan is about to start processing files in directory <code>path</code>. */
+        void onStartingDirectory(@NotNull File directoryPath);
+
+        /** Called to determine whether the implementation should process <code>filePath</code>. */
         boolean shouldProcess(@NotNull File file);
-        void onException(@NotNull File file, @NotNull Throwable throwable);
-        void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath);
-        void onCompleted();
 
-        void onProcessingStarting(@NotNull File file);
+        /** Called before extraction is performed on <code>filePath</code>. */
+        void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath);
+
+        /** Called when extraction on <code>filePath</code> completed without an exception. */
+        void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log);
+
+        /** Called when extraction on <code>filePath</code> resulted in an exception. */
+        void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log);
+
+        /** Called when all files have been processed. */
+        void onScanCompleted(@NotNull PrintStream log);
     }
 
     abstract static class FileHandlerBase implements FileHandler
     {
         private final Set<String> _supportedExtensions = new HashSet<String>(
             Arrays.asList(
-                "jpg", "jpeg", "png", "gif", "bmp", "ico",
+                "jpg", "jpeg", "png", "gif", "bmp", "ico", "webp", "pcx", "ai", "eps",
                 "nef", "crw", "cr2", "orf", "arw", "raf", "srw", "x3f", "rw2", "rwl",
                 "tif", "tiff", "psd", "dng"));
 
@@ -141,53 +181,48 @@ public class ProcessAllImagesInFolderUtility
         private int _errorCount = 0;
         private long _processedByteCount = 0;
 
+        public void onStartingDirectory(@NotNull File directoryPath)
+        {}
+
         public boolean shouldProcess(@NotNull File file)
         {
             String extension = getExtension(file);
             return extension != null && _supportedExtensions.contains(extension.toLowerCase());
         }
 
-        public void onProcessingStarting(@NotNull File file)
+        public void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath)
         {
             _processedFileCount++;
             _processedByteCount += file.length();
         }
 
-        public void onException(@NotNull File file, @NotNull Throwable throwable)
+        public void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log)
         {
             _exceptionCount++;
-
-            if (throwable instanceof ImageProcessingException) {
-                // this is an error in the Jpeg segment structure.  we're looking for bad handling of
-                // metadata segments.  in this case, we didn't even get a segment.
-                System.err.printf("%s: %s [Error Extracting Metadata]\n\t%s%n", throwable.getClass().getName(), file, throwable.getMessage());
-            } else {
-                // general, uncaught exception during processing of jpeg segments
-                System.err.printf("%s: %s [Error Extracting Metadata]%n", throwable.getClass().getName(), file);
-                throwable.printStackTrace(System.err);
-            }
+            log.printf("\t[%s] %s\n", throwable.getClass().getName(), throwable.getMessage());
         }
 
-        public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath)
+        public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
         {
             if (metadata.hasErrors()) {
-                System.err.println(file);
+                log.print(file);
+                log.print('\n');
                 for (Directory directory : metadata.getDirectories()) {
                     if (!directory.hasErrors())
                         continue;
                     for (String error : directory.getErrors()) {
-                        System.err.printf("\t[%s] %s%n", directory.getName(), error);
+                        log.printf("\t[%s] %s\n", directory.getName(), error);
                         _errorCount++;
                     }
                 }
             }
         }
 
-        public void onCompleted()
+        public void onScanCompleted(@NotNull PrintStream log)
         {
             if (_processedFileCount > 0) {
-                System.out.println(String.format(
-                    "Processed %,d files (%,d bytes) with %,d exceptions and %,d file errors",
+                log.print(String.format(
+                    "Processed %,d files (%,d bytes) with %,d exceptions and %,d file errors\n",
                     _processedFileCount, _processedByteCount, _exceptionCount, _errorCount
                 ));
             }
@@ -211,10 +246,53 @@ public class ProcessAllImagesInFolderUtility
      */
     static class TextFileOutputHandler extends FileHandlerBase
     {
+        /** Standardise line ending so that generated files can be more easily diffed. */
+        private static final String NEW_LINE = "\n";
+
+        @Override
+        public void onStartingDirectory(@NotNull File directoryPath)
+        {
+            super.onStartingDirectory(directoryPath);
+
+            // Delete any existing 'metadata' folder
+            File metadataDirectory = new File(directoryPath + "/metadata");
+            if (metadataDirectory.exists())
+                deleteRecursively(metadataDirectory);
+        }
+
+        private static void deleteRecursively(@NotNull File directory)
+        {
+            if (!directory.isDirectory())
+                throw new IllegalArgumentException("Must be a directory.");
+
+            if (directory.exists()) {
+                String[] list = directory.list();
+                if (list != null) {
+                    for (String item : list) {
+                        File file = new File(item);
+                        if (file.isDirectory())
+                            deleteRecursively(file);
+                        else
+                            file.delete();
+                    }
+                }
+            }
+
+            directory.delete();
+        }
+
+        @Override
+        public void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath)
+        {
+            super.onBeforeExtraction(file, log, relativePath);
+            log.print(file.getAbsoluteFile());
+            log.print(NEW_LINE);
+        }
+
         @Override
-        public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath)
+        public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
         {
-            super.onExtracted(file, metadata, relativePath);
+            super.onExtractionSuccess(file, metadata, relativePath, log);
 
             try {
                 PrintWriter writer = null;
@@ -227,25 +305,67 @@ public class ProcessAllImagesInFolderUtility
                         for (Directory directory : metadata.getDirectories()) {
                             if (!directory.hasErrors())
                                 continue;
-                            for (String error : directory.getErrors()) {
-                                writer.format("[ERROR: %s] %s\n", directory.getName(), error);
-                            }
+                            for (String error : directory.getErrors())
+                                writer.format("[ERROR: %s] %s%s", directory.getName(), error, NEW_LINE);
                         }
-                        writer.write("\n");
+                        writer.write(NEW_LINE);
                     }
 
-                    // Iterate through all values
+                    // Write tag values for each directory
                     for (Directory directory : metadata.getDirectories()) {
                         String directoryName = directory.getName();
+                        // Write the directory's tags
                         for (Tag tag : directory.getTags()) {
                             String tagName = tag.getTagName();
                             String description = tag.getDescription();
-                            writer.format("[%s - %s] %s = %s%n", directoryName, tag.getTagTypeHex(), tagName, description);
+                            if (description == null)
+                                description = "";
+                            // Skip the file write-time as this changes based on the time at which the regression test image repository was cloned
+                            if (directory instanceof FileMetadataDirectory && tag.getTagType() == FileMetadataDirectory.TAG_FILE_MODIFIED_DATE)
+                                description = "<omitted for regression testing as checkout dependent>";
+                            writer.format("[%s - %s] %s = %s%s", directoryName, tag.getTagTypeHex(), tagName, description, NEW_LINE);
                         }
-                        if (directory.getTagCount() != 0) {
-                            writer.write('\n');
+                        if (directory.getTagCount() != 0)
+                            writer.write(NEW_LINE);
+                        // Special handling for XMP directory data
+                        if (directory instanceof XmpDirectory) {
+                            boolean wrote = false;
+                            XmpDirectory xmpDirectory = (XmpDirectory)directory;
+                            XMPMeta xmpMeta = xmpDirectory.getXMPMeta();
+                            try {
+                                XMPIterator iterator = xmpMeta.iterator();
+                                while (iterator.hasNext()) {
+                                    XMPPropertyInfo prop = (XMPPropertyInfo)iterator.next();
+                                    String ns = prop.getNamespace();
+                                    String path = prop.getPath();
+                                    String value = prop.getValue();
+
+                                    if (ns == null)
+                                        ns = "";
+                                    if (path == null)
+                                        path = "";
+
+                                    final int MAX_XMP_VALUE_LENGTH = 512;
+                                    if (value == null)
+                                        value = "";
+                                    else if (value.length() > MAX_XMP_VALUE_LENGTH)
+                                        value = String.format("%s <truncated from %d characters>", value.substring(0, MAX_XMP_VALUE_LENGTH), value.length());
+
+                                    writer.format("[XMPMeta - %s] %s = %s%s", ns, path, value, NEW_LINE);
+                                    wrote = true;
+                                }
+                            } catch (XMPException e) {
+                                e.printStackTrace();
+                            }
+                            if (wrote)
+                                writer.write(NEW_LINE);
                         }
                     }
+
+                    // Write file structure
+                    writeHierarchyLevel(metadata, writer, null, 0);
+
+                    writer.write(NEW_LINE);
                 } finally {
                     closeWriter(writer);
                 }
@@ -254,22 +374,44 @@ public class ProcessAllImagesInFolderUtility
             }
         }
 
+        private static void writeHierarchyLevel(@NotNull Metadata metadata, @NotNull PrintWriter writer, @Nullable Directory parent, int level)
+        {
+            final int indent = 4;
+
+            for (Directory child : metadata.getDirectories()) {
+                if (parent == null) {
+                    if (child.getParent() != null)
+                        continue;
+                } else if (!parent.equals(child.getParent())) {
+                    continue;
+                }
+
+                for (int i = 0; i < level*indent; i++) {
+                    writer.write(' ');
+                }
+                writer.write("- ");
+                writer.write(child.getName());
+                writer.write(NEW_LINE);
+                writeHierarchyLevel(metadata, writer, child, level + 1);
+            }
+        }
+
         @Override
-        public void onException(@NotNull File file, @NotNull Throwable throwable)
+        public void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log)
         {
-            super.onException(file, throwable);
+            super.onExtractionError(file, throwable, log);
 
             try {
                 PrintWriter writer = null;
                 try {
                     writer = openWriter(file);
-                    throwable.printStackTrace(writer);
-                    writer.write('\n');
+                    writer.write("EXCEPTION: " + throwable.getMessage() + NEW_LINE);
+                    writer.write(NEW_LINE);
                 } finally {
                     closeWriter(writer);
                 }
             } catch (IOException e) {
-                System.err.printf("IO exception writing metadata file: %s%n", e.getMessage());
+                log.printf("IO exception writing metadata file: %s%s", e.getMessage(), NEW_LINE);
             }
         }
 
@@ -281,10 +423,25 @@ public class ProcessAllImagesInFolderUtility
             if (!metadataDir.exists())
                 metadataDir.mkdir();
 
-            String outputPath = String.format("%s/metadata/%s.txt", file.getParent(), file.getName().toLowerCase());
-            FileWriter writer = new FileWriter(outputPath, false);
-            writer.write("FILE: " + file.getName() + "\n");
-            writer.write('\n');
+            String outputPath = String.format("%s/metadata/%s.txt", file.getParent(), file.getName());
+            Writer writer = new OutputStreamWriter(
+                new FileOutputStream(outputPath),
+                "UTF-8"
+            );
+            writer.write("FILE: " + file.getName() + NEW_LINE);
+
+            // Detect file type
+            BufferedInputStream stream = null;
+            try {
+                stream = new BufferedInputStream(new FileInputStream(file));
+                FileType fileType = FileTypeDetector.detectFileType(stream);
+                writer.write(String.format("TYPE: %s" + NEW_LINE, fileType.toString().toUpperCase()));
+                writer.write(NEW_LINE);
+            } finally {
+                if (stream != null) {
+                    stream.close();
+                }
+            }
 
             return new PrintWriter(writer);
         }
@@ -292,8 +449,8 @@ public class ProcessAllImagesInFolderUtility
         private static void closeWriter(@Nullable Writer writer) throws IOException
         {
             if (writer != null) {
-                writer.write("Generated using metadata-extractor\n");
-                writer.write("https://drewnoakes.com/code/exif/\n");
+                writer.write("Generated using metadata-extractor" + NEW_LINE);
+                writer.write("https://drewnoakes.com/code/exif/" + NEW_LINE);
                 writer.flush();
                 writer.close();
             }
@@ -308,7 +465,7 @@ public class ProcessAllImagesInFolderUtility
         private final Map<String, String> _extensionEquivalence = new HashMap<String, String>();
         private final Map<String, List<Row>> _rowListByExtension = new HashMap<String, List<Row>>();
 
-        class Row
+        static class Row
         {
             final File file;
             final Metadata metadata;
@@ -325,9 +482,9 @@ public class ProcessAllImagesInFolderUtility
                 this.metadata = metadata;
                 this.relativePath = relativePath;
 
-                ExifIFD0Directory ifd0Dir = metadata.getDirectory(ExifIFD0Directory.class);
-                ExifSubIFDDirectory subIfdDir = metadata.getDirectory(ExifSubIFDDirectory.class);
-                ExifThumbnailDirectory thumbDir = metadata.getDirectory(ExifThumbnailDirectory.class);
+                ExifIFD0Directory ifd0Dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+                ExifSubIFDDirectory subIfdDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
+                ExifThumbnailDirectory thumbDir = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
                 if (ifd0Dir != null) {
                     manufacturer = ifd0Dir.getDescription(ExifIFD0Directory.TAG_MAKE);
                     model = ifd0Dir.getDescription(ExifIFD0Directory.TAG_MODEL);
@@ -338,8 +495,8 @@ public class ProcessAllImagesInFolderUtility
                     hasMakernoteData = subIfdDir.containsTag(ExifSubIFDDirectory.TAG_MAKERNOTE);
                 }
                 if (thumbDir != null) {
-                    Integer width = thumbDir.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
-                    Integer height = thumbDir.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
+                    Integer width = thumbDir.getInteger(ExifThumbnailDirectory.TAG_IMAGE_WIDTH);
+                    Integer height = thumbDir.getInteger(ExifThumbnailDirectory.TAG_IMAGE_HEIGHT);
                     thumbnail = width != null && height != null
                         ? String.format("Yes (%s x %s)", width, height)
                         : "Yes";
@@ -347,6 +504,7 @@ public class ProcessAllImagesInFolderUtility
                 for (Directory directory : metadata.getDirectories()) {
                     if (directory.getClass().getName().contains("Makernote")) {
                         makernote = directory.getName().replace("Makernote", "").trim();
+                        break;
                     }
                 }
                 if (makernote == null) {
@@ -361,9 +519,9 @@ public class ProcessAllImagesInFolderUtility
         }
 
         @Override
-        public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath)
+        public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
         {
-            super.onExtracted(file, metadata, relativePath);
+            super.onExtractionSuccess(file, metadata, relativePath, log);
 
             String extension = getExtension(file);
 
@@ -374,7 +532,7 @@ public class ProcessAllImagesInFolderUtility
             // Sanitise the extension
             extension = extension.toLowerCase();
             if (_extensionEquivalence.containsKey(extension))
-                extension =_extensionEquivalence.get(extension);
+                extension = _extensionEquivalence.get(extension);
 
             List<Row> list = _rowListByExtension.get(extension);
             if (list == null) {
@@ -385,9 +543,9 @@ public class ProcessAllImagesInFolderUtility
         }
 
         @Override
-        public void onCompleted()
+        public void onScanCompleted(@NotNull PrintStream log)
         {
-            super.onCompleted();
+            super.onScanCompleted(log);
 
             OutputStream outputStream = null;
             PrintStream stream = null;
@@ -415,12 +573,14 @@ public class ProcessAllImagesInFolderUtility
             Writer writer = new OutputStreamWriter(stream);
             writer.write("# Image Database Summary\n\n");
 
-            for (String extension : _rowListByExtension.keySet()) {
+            for (Map.Entry<String, List<Row>> entry : _rowListByExtension.entrySet()) {
+                String extension = entry.getKey();
                 writer.write("## " + extension.toUpperCase() + " Files\n\n");
 
                 writer.write("File|Manufacturer|Model|Dir Count|Exif?|Makernote|Thumbnail|All Data\n");
                 writer.write("----|------------|-----|---------|-----|---------|---------|--------\n");
-                List<Row> rows = _rowListByExtension.get(extension);
+
+                List<Row> rows = entry.getValue();
 
                 // Order by manufacturer, then model
                 Collections.sort(rows, new Comparator<Row>() {
@@ -432,7 +592,7 @@ public class ProcessAllImagesInFolderUtility
                 });
 
                 for (Row row : rows) {
-                    writer.write(String.format("[%s](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/%s)|%s|%s|%d|%s|%s|%s|[metadata](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/metadata/%s.txt)%n",
+                    writer.write(String.format("[%s](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/%s)|%s|%s|%d|%s|%s|%s|[metadata](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/metadata/%s.txt)\n",
                             row.file.getName(),
                             row.relativePath,
                             StringUtil.urlEncode(row.file.getName()),
@@ -453,6 +613,66 @@ public class ProcessAllImagesInFolderUtility
         }
     }
 
+    /**
+     * Keeps track of unknown tags.
+     */
+    static class UnknownTagHandler extends FileHandlerBase
+    {
+        private HashMap<String, HashMap<Integer, Integer>> _occurrenceCountByTagByDirectory = new HashMap<String, HashMap<Integer, Integer>>();
+
+        @Override
+        public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
+        {
+            super.onExtractionSuccess(file, metadata, relativePath, log);
+
+            for (Directory directory : metadata.getDirectories()) {
+                for (Tag tag : directory.getTags()) {
+
+                    // Only interested in unknown tags (those without names)
+                    if (tag.hasTagName())
+                        continue;
+
+                    HashMap<Integer, Integer> occurrenceCountByTag = _occurrenceCountByTagByDirectory.get(directory.getName());
+                    if (occurrenceCountByTag == null) {
+                        occurrenceCountByTag = new HashMap<Integer, Integer>();
+                        _occurrenceCountByTagByDirectory.put(directory.getName(), occurrenceCountByTag);
+                    }
+
+                    Integer count = occurrenceCountByTag.get(tag.getTagType());
+                    if (count == null) {
+                        count = 0;
+                        occurrenceCountByTag.put(tag.getTagType(), 0);
+                    }
+
+                    occurrenceCountByTag.put(tag.getTagType(), count + 1);
+                }
+            }
+        }
+
+        @Override
+        public void onScanCompleted(@NotNull PrintStream log)
+        {
+            super.onScanCompleted(log);
+
+            for (Map.Entry<String, HashMap<Integer, Integer>> pair1 : _occurrenceCountByTagByDirectory.entrySet()) {
+                String directoryName = pair1.getKey();
+                List<Map.Entry<Integer, Integer>> counts = new ArrayList<Map.Entry<Integer, Integer>>(pair1.getValue().entrySet());
+                Collections.sort(counts, new Comparator<Map.Entry<Integer, Integer>>()
+                {
+                    public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2)
+                    {
+                        return o2.getValue().compareTo(o1.getValue());
+                    }
+                });
+                for (Map.Entry<Integer, Integer> pair2 : counts) {
+                    Integer tagType = pair2.getKey();
+                    Integer count = pair2.getValue();
+                    log.format("%s, 0x%04X, %d\n", directoryName, tagType, count);
+                }
+            }
+        }
+    }
+
     /**
      * Does nothing with the output except enumerate it in memory and format descriptions. This is useful in order to
      * flush out any potential exceptions raised during the formatting of extracted value descriptions.
@@ -460,9 +680,9 @@ public class ProcessAllImagesInFolderUtility
     static class BasicFileHandler extends FileHandlerBase
     {
         @Override
-        public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath)
+        public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
         {
-            super.onExtracted(file, metadata, relativePath);
+            super.onExtractionSuccess(file, metadata, relativePath, log);
 
             // Iterate through all values, calling toString to flush out any formatting exceptions
             for (Directory directory : metadata.getDirectories()) {
diff --git a/Source/com/drew/tools/ProcessUrlUtility.java b/Source/com/drew/tools/ProcessUrlUtility.java
index 680c9da..8308e7a 100644
--- a/Source/com/drew/tools/ProcessUrlUtility.java
+++ b/Source/com/drew/tools/ProcessUrlUtility.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -67,7 +67,7 @@ public class ProcessUrlUtility
         } catch (ImageProcessingException e) {
             // this is an error in the Jpeg segment structure.  we're looking for bad handling of
             // metadata segments.  in this case, we didn't even get a segment.
-            System.err.printf("%s: %s [Error Extracting Metadata]\n\t%s%n", e.getClass().getName(), url, e.getMessage()); return;
+            System.err.printf("%s: %s [Error Extracting Metadata]%n\t%s%n", e.getClass().getName(), url, e.getMessage()); return;
         } catch (Throwable t) {
             // general, uncaught exception during processing of jpeg segments
             System.err.printf("%s: %s [Error Extracting Metadata]%n", t.getClass().getName(), url);
diff --git a/Source/com/drew/tools/package-info.java b/Source/com/drew/tools/package-info.java
new file mode 100644
index 0000000..54d0649
--- /dev/null
+++ b/Source/com/drew/tools/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Contains classes used internally by the library, that should not be used in client code and is not included in
+ * distributions.
+ */
+package com.drew.tools;
diff --git a/Source/com/drew/tools/package.html b/Source/com/drew/tools/package.html
deleted file mode 100644
index 8e713fd..0000000
--- a/Source/com/drew/tools/package.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes used internally by the library, that should not be used in client code (not included in distributions).
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
diff --git a/Tests/Data/withTypicalHuffman.jpg b/Tests/Data/withTypicalHuffman.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..a8de094e9eb9bf8a842ba3bd3d85e32412899dc1
GIT binary patch
literal 1570
zcmbW!dpOg390%~<ZniPBY_*BBS=v-9Gb|yw%_UD~A{5D`u}&p-ahy=Zn&d9!ahoAj
zh;pfe7_p<rB~s)vp^HxLSvt<tdCqyB^Vd1&{e8ZlKR(~j^L@Ur=ezb|Z4}sGZDC~r
zfWQC%k}hEF6<`V=WMtqnFa#V9M<NljD2zNxP7a0NsHBKdRaV=os;r{2jYQqCjkrr)
zMTM+K*|k@TPN%Ew*k{O~8Bn$9wDn6sNF)*^hr-Fr<7fmG0_{I%tr0*Y00Y1a0wMxn
zGzfwQt+fDZ(s#l@-vay@5Euf5$-ogvSvl!I^#%Y8f<VAf2n+^=N~a^F=KvH9+epwl
zD5J!3hZFrUw5X&!gt|#Z6V|T#6G_{{KN=~!X)_M5yj^1lnW9P8(bZ$@(>FCUw_sXY
zSs$@?IO^!+e9Y796x-W}b2=a}C^#haLfECtF|k+T;<?EwscGpMnK!cX3;2bCqT-U<
zl~wnvYaTqTt!sYL(kg6gf7;R0+xNVG;Kks}v2pRlo5`u^x9?`><`)*1mOrmZ)?FX~
z@|`8E-(ml7p`|V`6bgaD*IgiRh;&2HP#8f=X5&E?+}%%!NQ**XOp@{{nvm++cAu~w
z{@t>hNc6Gov+J~PvVRAQ{$H}cV1K(_0w@Sb`gssEZ~$1yIF*1Oj?U+$dq%=bZujeF
z#|rNB>s=qZ%(zIIzyGD*m-pM*Gpl>TRa;xmKN7LYrfYyw@?O{ljfC>|$|s6;i-;Y~
zc2||QKN)kI#G?#D-@AVFyT+Sf`Fg(Avf@siG4S*ZK+!s^nVRr<V9Y{4=UfAKx*$m-
z-Yel!0jgBt@)29ZsMhk9Rt+)-W7Jfe86>Nnc(;(j^}w0ax!1WI42s9)lpq92mWXyi
z)_NnB#tkNy;IE47{fyG^LGcAw1B*mFBU$;z$nYtZ5IlB#+|a_3>Xmc&Mq*~Mcc8&0
zLzyrIrrT+WlFL0;Q|okXbXv-C363$;sG=yWW%pbh&$bWPl|FDh+1_P8<=iiNv%m%T
z-NV*h<whLmY>c2luDtW-t<`!ptQYUob#3Wra%6r&6CAhL-o|otQIuO*4%vL|<&U~k
zv}7SI6bMu7n|k|$d0us4O!bgs%CJjI)QNH9ymq)@J-a65gQP24=TYCm?kgFZ=%#1;
z)I<?GZ8(BSs_%qRSLivj%Eng(dj`Iip%RP7%RCCIs<^`EC*vv{CaP{#so8~RWpb;-
zPqUZRiCN~B<^)V*9G8P7V0tJZJf=s}gu=JOiC8vV3aYzo%T0N+(Dco`mAXR}*kk~_
zLc93v(DFiKoA~&`D5ZmvoXrbWcGaIfd2=%MuvjP)t~SjS+t%GrvA#ay+-BH;LhkN`
zHK#mf2}kuMyz@JcPWHT(4A0yVkDww;0ZYar)L-7*dnq-pH6Vp#d(Vc4G@gF4x>#1L
zQ?+P5ViQw7{AjV#<yc2$vskn9b)lfJL93Hg(GoYOQco6&-*JRBjx~IFdh9KqIllV-
zEn6Sv)x+Wv2&LejU<SHYLgk$3?Qr>($o%lJ^miQvzN1|#{!p*2<@})8h>nP<YTW6)
zV!ylGNr-T=Nt(_JSbeAJ(2dD$ZN-8Hat_p2B>J8e4d!Q5M_g&Izo}VxP17Ac<B<2z
zU+<?}NetGVe;zqxwX6~U+{YTykkO^v`&jrPtXPsIs_UZ?S6I=!V62I)$(y=%ThkK4
z94XL^{UFgOsORpz;)MP*^k{>iHlsdu+3m~3kz>xd8EQ)yJ9|=+IP9vBR^1ue_VH}=
zSLSC2{Hm#6SL5x70yWWOh1`P@!9-RXwvXzP<McQ)dw@2&Bp)Sl&<zS6)>tx}8}{Mr
n95q(g_k0u2tR-b7hNET}&vJ`4Py~EWE7C+2n@i4BTpRoYHBYC?

literal 0
HcmV?d00001

diff --git a/Tests/Data/withXmp.jpg b/Tests/Data/withXmp.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9aa65df4f6e4fe933a2308a32d6fd5d707bf661e
GIT binary patch
literal 12666
zcmeHNe^e7k9-nN25CjQmTa-nXfR!pFBp^T11Pg&c&<K(!$hGQ{EMZGX!tMqFdVNo&
zr(VS>R;xahYD;T>*{3a4^w}!d(w;3nYESV*tF?+++iTSmZLQqwf*~O7^ZxAX4oTRV
z@6Y+ncV_qP`}B47oduFyon8k70s+W@f1vMsM3P_TU>tOr!-*xx;z3G=-XIbI*aSCF
zK`{`9P0B_E03bkwBDKIrYn1B?Q6w4oE8`W@;^Gx?N+pU|fjD!nA~kWEQjvfnK|o+c
z5q}_n*97oQpJOx?=+jYT4)8NF6lu2-6pG}7AZ<>8-e}6xVMbHBHYW>3Xdq12<)g@x
zKmbsr82Hc7n~F2$WaVVdL=iXWNO#fD9-mBO2ApBaNSwW>At(&?2)l*Gm@)#ZAnZ`V
z;;6Qlkv1E~P&jFaF)>FeC)VkWSh~$g+6l~9PO^eR2W7KhT9*?e?br;GsV#94WrQsj
z8weMRIqX<2Zg=5S4W?AaV(iRiSbu~0V);3FraVlJ4V%xft{ke$vk8W&!Soflm7r0?
z2Z*O7B&DV*QWVorWE>D0XXsJH0#TS_LJ>QV+;2jHm<L4xMVJvVUH}AfC=$aVoX-9F
z4xr90a^Jbgq%1dC3V^`?rob}r146+VxcmTo0e~cJISJV6fE5uu<_ZdY;aC9dE8!QQ
zSKyC?z2X~>0FMBV0FMBV0FMBV0FMBV0FMBV0FMBV0FS_*0f7g7N%6;cRRdQ{F!;73
z7-LtEf7`*Lz&9QN9swQ!9swQ!9swQ!9swQ!9swQ!9swSKKNA9d=fufHhJ2us%k8ub
zw>U}&nb}bxcjHdELKZIv>gjGLZZ0Pntc0+Vc8#>{oz?@bA4)OOqLCI(GsGL5TB3~1
zsig>G?JSeIw%nX*kxoxjyH)NAX9dCFn7hJer&Vr^lyk3w*X*!dig7ATxkd^zz_bpE
zz!GH&nL?VTrYxnZf=u0jDeP&aWenqlSyoq9%c>J(4$3N5q^73I<CSuyG7f6Q(KU7k
zcgNZ3r~wC=1Z}2BCqqIW4C@dtakv<b6vpKea^52ld3+B+z$4~xxM!qyGUh}$F*aL|
z9gJb-GNKua+*^Y)<)X2(<)U&-ntmcpwq9rQ_<%lTR+eQ$awHpW5v*h>GbFmj?9q3+
zC>s~kVwMv&0<wn~8tN)Mff%xM5VavQV<kh}5|B9KaXdtol?+jJ+FZ1^s*5JrAubb}
zrBn{5F+|^5L)sj6$UIVdtVWtC)Uoud4On9y_*I!WLue@i{=$kIWN(?`<5k=To67x@
zdYS0$kZ|QPvnvm?^_HZ&m@)@N=R0VUm1?B-m|aqA*vhE1cm+Y>I?_hi;T7v<xJDjD
z!YzT@26Yk>Qc|X+D3l9~S?RK;o!0wpxTP<^?Xc<-q?pC5Vo1gYy?Y2%-q~s$E;~cj
zXru#NG%4m?U8(`G)?tATAot4K)aY<gW_DPY?>(GD;1(MskwL)&lG297oar#*{m6N{
z)shU%a*+1M&8&d5mN5|jVT3YHW*OX@51z^&<E85mTqwdq!ZT+$A#R8ctZb`;U1g*9
zu!+L$bOlM%uuKbxQi3Av?0UxCUsC;rYQW101FQtw?^SC5K4N#2iDYX!J{~e5xzF!y
zCoRq$qJ<R65ENWbmP<KO8aA8GL2(7Hk?L?8O^l?(ZYm=I>5#Tru8I=Y-w0Zc3a4j>
z-Y?dXZpK9sdJ8=C$Wn4-A6afQJebHTLhGo4A+2z>{W+xt`uB4n)$GZo;_@}RoK72b
z#C7zRG!H9OE&pw$K|lBIBr47icNpU~ZU4HG1nr54VceZ91Gjn&4Gh}IG)SXV%h@{>
zyuRlgO1U9#8<^CCMpVNShIW-aO_&*PmuwK|5tDlc)$$QE)x%G`e1f7K@Wg}L-D5d8
zsUhwj#FWj>%pUCyYPdTiQRy`_h|#da<Tvs1w&&(HG)}*r*AoI4tbY_fc)*@o?s<Yq
z8+-_YbX|?K8b06jodR0mH)@pcC?7vxUth7<&p%*XV8H0n0pml)28|1!5HT@)0*YeM
zvdNe<HVQ>!k{^qWS0pATModoCq$t(0ghVClL?9N614ajg1_p*IBhg6Z|K0iyfnYzN
z1{R?}3XoudFj&xc1fY->iwGfM+S3pr@DYec`T9YrLIMOL0V482e0=&<1OO3=f_*|F
z73s4_jg{h+<90`7EZN@qsIg?};UA$%C}dx=1nj}anLtToFbpLSLI0j6K!PKMA&T^|
zv!#_w`u+_9gitwH7!0O^8!xu(-u=a*WksD0Jt%m5{oC2b?XNfg@G=z^xjg7v<JQ+s
z&%M?0eepAo90-3dxb+vs#_d6OzKCAv^X=xOy>F4VTi(aV?aVFe+Hy3gZNZiCW5RFI
z^Lta;1FpsFJ^Y{0qSl@=_s8`c@eKv@(6@rFgR`Bht*06#`(wVfbSC9=PizlpZd)rh
zq=`S;H4``ct8~k+hHW|bS)w&UvsSVO)I~okse9IMkK~0`$(p4PXM}9b2$5hh&-ur$
zoN<2tH-DX?ym{o(H>V%F9u;(WVYi>}uFAHZpUjs<ePvnt*}BWuQ*PJeTd%4|KOebG
zyQ1gn2X!Z6lJmc8T6Kh=r$4lIS$zjM`^=jgs@DCgI<7uCsr)aSriLwVY3lxU!A9Yc
ziHi@&YWFW{nt6jX=pJrKx}!0jZVC^l>L(jS2Fb&T_5f7zbrjlq+CA}+>)&_n>m_$p
z^=#5tuG$91wDqRm%6U%f%6;hVkElg+OBR-2&6zOqiPX!-rd)2!d+YMC!e+@LRNYdP
zE!>tGQTgNkxr$Fe`uwi%EVpeXvE<at@}9dz<0GP$cC|jGs+|2smsA$#2<h3<(%z!U
ztsMRIcaxJp*&|uAG2^f}GBYwVzAhSjc1>L!glLJ-KlWUvxMZj8pI>FK#1~sX|B_ho
zehvt{X1RN@_U57XEAMoIO<4(#b><11j_pbkAI$&s_D@T;Ex#N5^Y-Qf!xU}p7W(AE
zLk$Pw=TFEB>1=bNzpQflAOAeJ_stKQ_w}}6aZb^))n`{@O~#)$WR|?UE_zniq>n`}
zQO-WleIjntYd7o9#ICP6ad6&-DgQW$yN^D3=JlQP=7(NrDEh`65qY$`v%bA0vygDD
z1M8Qrc>O|-uIlmdA78z+b9T|xg{H0TNOI%fE)f4!zj~1zAMx&)<9%RiP{Gu9a|4dW
z<eGb$#vYtol5%m>Q;mCOG{2!(8YZp@-yGJszwKX7Mdb!`j4S@(#MZ{F(3>~9pUhox
z?L+PEMY7Fbp07I>e5v`nj^3Ycymvk|P~5Tp?AV69mY5BDcNF8d<sC^oeoo$Zz3lds
zJ;z@ziE3UQ7<%cU#hdG2q}RMq6MZXgWq8D~!spXmukP)d6jp4?Ub47})_wKP#V7he
z{)XGHs7H5a+upeNOT^zVUEaE@aMd$`3%_1_CvN;5t$FpK<BetQ-4_ZTInbhQm|L~$
zXk|;<?%I;-c`y3DRONg1z3!U_PrY*H{0Z5`9f!q-L+O*x&ooT@wxOVRpOyT0ktkCX
SjipDgYtW}%iP_=Tck+Ms4%t)y

literal 0
HcmV?d00001

diff --git a/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java b/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java
index 17e5119..cda6d54 100644
--- a/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java
+++ b/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -23,13 +23,18 @@ package com.drew.imaging.jpeg;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.exif.ExifSubIFDDirectory;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable;
+import com.drew.metadata.xmp.XmpDirectory;
 import org.junit.Test;
 
 import java.io.File;
 import java.io.FileInputStream;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 /**
  * @author Drew Noakes https://drewnoakes.com
@@ -48,10 +53,39 @@ public class JpegMetadataReaderTest
         validate(JpegMetadataReader.readMetadata(new FileInputStream((new File("Tests/Data/withExif.jpg")))));
     }
 
+    @Test
+    public void testExtractXmpMetadata() throws Exception
+    {
+        Metadata metadata = JpegMetadataReader.readMetadata(new File("Tests/Data/withXmp.jpg"));
+        Directory directory = metadata.getFirstDirectoryOfType(XmpDirectory.class);
+        assertNotNull(directory);
+        directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class);
+        assertNotNull(directory);
+        assertTrue(((HuffmanTablesDirectory) directory).isOptimized());
+    }
+
+    @Test
+    public void testTypicalHuffman() throws Exception
+    {
+        Metadata metadata = JpegMetadataReader.readMetadata(new File("Tests/Data/withTypicalHuffman.jpg"));
+        Directory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class);
+        assertNotNull(directory);
+        assertTrue(((HuffmanTablesDirectory) directory).isTypical());
+        assertFalse(((HuffmanTablesDirectory) directory).isOptimized());
+        for (int i = 0; i < ((HuffmanTablesDirectory) directory).getNumberOfTables(); i++) {
+            HuffmanTable table = ((HuffmanTablesDirectory) directory).getTable(i);
+            assertTrue(table.isTypical());
+            assertFalse(table.isOptimized());
+        }
+    }
+
     private void validate(Metadata metadata)
     {
-        Directory directory = metadata.getDirectory(ExifSubIFDDirectory.class);
+        Directory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
         assertNotNull(directory);
         assertEquals("80", directory.getString(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
+        directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class);
+        assertNotNull(directory);
+        assertTrue(((HuffmanTablesDirectory) directory).isOptimized());
     }
 }
diff --git a/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java b/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java
index d270343..7474b2b 100644
--- a/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java
+++ b/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java b/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java
index 7d02a17..edea9ab 100644
--- a/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java
+++ b/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import org.junit.Test;
 
 import java.io.File;
 import java.util.Arrays;
+import java.util.Collections;
 
 import static org.junit.Assert.*;
 
@@ -85,6 +86,7 @@ public class JpegSegmentReaderTest
         assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPC));
         assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPF));
         assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.COM));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.DAC));
         assertEquals(4, segmentData.getSegmentCount(JpegSegmentType.DHT));
         assertEquals(2, segmentData.getSegmentCount(JpegSegmentType.DQT));
         assertEquals(1, segmentData.getSegmentCount(JpegSegmentType.SOF0));
@@ -127,6 +129,34 @@ public class JpegSegmentReaderTest
                 segmentData.getSegment(JpegSegmentType.APP2));
     }
 
+    @Test
+    public void testReadDhtSegment() throws Exception
+    {
+        JpegSegmentData segmentData = JpegSegmentReader.readSegments(
+            new File("Tests/Data/withExifAndIptc.jpg"),
+            Collections.singletonList(JpegSegmentType.DHT));
+
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP0));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP1));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP2));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPD));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPE));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP3));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP4));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP5));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP6));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP7));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP8));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP9));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPA));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPB));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPC));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPF));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.COM));
+        assertEquals(4, segmentData.getSegmentCount(JpegSegmentType.DHT));
+        assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.SOF0));
+    }
+
     @Test
     public void testLoadJpegWithoutExifDataReturnsNull() throws Exception
     {
diff --git a/Tests/com/drew/imaging/png/PngChunkReaderTest.java b/Tests/com/drew/imaging/png/PngChunkReaderTest.java
index c0af9cb..d9ba01b 100644
--- a/Tests/com/drew/imaging/png/PngChunkReaderTest.java
+++ b/Tests/com/drew/imaging/png/PngChunkReaderTest.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.Iterables;
diff --git a/Tests/com/drew/imaging/png/PngChunkTypeTest.java b/Tests/com/drew/imaging/png/PngChunkTypeTest.java
index 12b0a0e..af8893c 100644
--- a/Tests/com/drew/imaging/png/PngChunkTypeTest.java
+++ b/Tests/com/drew/imaging/png/PngChunkTypeTest.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import org.junit.Test;
@@ -15,7 +35,7 @@ public class PngChunkTypeTest
         try {
             new PngChunkType("TooLong");
             fail("Expecting exception");
-        } catch (IllegalArgumentException ex) {
+        } catch (PngProcessingException ex) {
             assertEquals("PNG chunk type identifier must be four bytes in length", ex.getMessage());
         }
     }
@@ -26,7 +46,7 @@ public class PngChunkTypeTest
         try {
             new PngChunkType("foo");
             fail("Expecting exception");
-        } catch (IllegalArgumentException ex) {
+        } catch (PngProcessingException ex) {
             assertEquals("PNG chunk type identifier must be four bytes in length", ex.getMessage());
         }
     }
@@ -40,7 +60,7 @@ public class PngChunkTypeTest
             try {
                 new PngChunkType(invalidString);
                 fail("Expecting exception");
-            } catch (IllegalArgumentException ex) {
+            } catch (PngProcessingException ex) {
                 assertEquals("PNG chunk type identifier may only contain alphabet characters", ex.getMessage());
             }
         }
diff --git a/Tests/com/drew/imaging/png/PngMetadataReaderTest.java b/Tests/com/drew/imaging/png/PngMetadataReaderTest.java
index 0abeeae..6bf8809 100644
--- a/Tests/com/drew/imaging/png/PngMetadataReaderTest.java
+++ b/Tests/com/drew/imaging/png/PngMetadataReaderTest.java
@@ -1,15 +1,37 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.imaging.png;
 
 import com.drew.lang.KeyValuePair;
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.png.PngDirectory;
 import org.junit.Test;
 
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
 import java.util.List;
+import java.util.Locale;
 import java.util.TimeZone;
 
 import static org.junit.Assert.*;
@@ -19,14 +41,6 @@ import static org.junit.Assert.*;
  */
 public class PngMetadataReaderTest
 {
-    @NotNull
-    public static <T extends Directory> T processFile(@NotNull String filePath, @NotNull Class<T> directoryClass) throws IOException, PngProcessingException
-    {
-        T directory = processFile(filePath).getDirectory(directoryClass);
-        assertNotNull(directory);
-        return directory;
-    }
-
     @NotNull
     private static Metadata processFile(@NotNull String filePath) throws PngProcessingException, IOException
     {
@@ -49,25 +63,53 @@ public class PngMetadataReaderTest
         try {
             TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
 
-            PngDirectory directory = processFile("Tests/Data/gimp-8x12-greyscale-alpha-time-background.png", PngDirectory.class);
-
-            assertEquals(8, directory.getInt(PngDirectory.TAG_IMAGE_WIDTH));
-            assertEquals(12, directory.getInt(PngDirectory.TAG_IMAGE_HEIGHT));
-            assertEquals(8, directory.getInt(PngDirectory.TAG_BITS_PER_SAMPLE));
-            assertEquals(4, directory.getInt(PngDirectory.TAG_COLOR_TYPE));
-            assertEquals(0, directory.getInt(PngDirectory.TAG_COMPRESSION_TYPE));
-            assertEquals(0, directory.getInt(PngDirectory.TAG_FILTER_METHOD));
-            assertEquals(0, directory.getInt(PngDirectory.TAG_INTERLACE_METHOD));
-            assertEquals(0.45455, directory.getDouble(PngDirectory.TAG_GAMMA), 0.00001);
-            assertArrayEquals(new byte[]{0, 52}, directory.getByteArray(PngDirectory.TAG_BACKGROUND_COLOR));
+            Metadata metadata = processFile("Tests/Data/gimp-8x12-greyscale-alpha-time-background.png");
+            Collection<PngDirectory> directories = metadata.getDirectoriesOfType(PngDirectory.class);
+
+            assertNotNull(directories);
+            assertEquals(6, directories.size());
+
+            PngDirectory[] dirs = new PngDirectory[directories.size()];
+            directories.toArray(dirs);
+
+            assertEquals(PngChunkType.IHDR, dirs[0].getPngChunkType());
+            assertEquals(8, dirs[0].getInt(PngDirectory.TAG_IMAGE_WIDTH));
+            assertEquals(12, dirs[0].getInt(PngDirectory.TAG_IMAGE_HEIGHT));
+            assertEquals(8, dirs[0].getInt(PngDirectory.TAG_BITS_PER_SAMPLE));
+            assertEquals(4, dirs[0].getInt(PngDirectory.TAG_COLOR_TYPE));
+            assertEquals(0, dirs[0].getInt(PngDirectory.TAG_COMPRESSION_TYPE));
+            assertEquals(0, dirs[0].getInt(PngDirectory.TAG_FILTER_METHOD));
+            assertEquals(0, dirs[0].getInt(PngDirectory.TAG_INTERLACE_METHOD));
+
+            assertEquals(PngChunkType.gAMA, dirs[1].getPngChunkType());
+            assertEquals(0.45455, dirs[1].getDouble(PngDirectory.TAG_GAMMA), 0.00001);
+
+            assertEquals(PngChunkType.bKGD, dirs[2].getPngChunkType());
+            assertArrayEquals(new byte[]{0, 52}, dirs[2].getByteArray(PngDirectory.TAG_BACKGROUND_COLOR));
+
             //noinspection ConstantConditions
-            assertEquals("Tue Jan 01 04:08:30 GMT 2013", directory.getDate(PngDirectory.TAG_LAST_MODIFICATION_TIME).toString());
+            assertEquals(PngChunkType.pHYs, dirs[3].getPngChunkType());
+            assertEquals(1, dirs[3].getInt(PngDirectory.TAG_UNIT_SPECIFIER));
+            assertEquals(2835, dirs[3].getInt(PngDirectory.TAG_PIXELS_PER_UNIT_X));
+            assertEquals(2835, dirs[3].getInt(PngDirectory.TAG_PIXELS_PER_UNIT_Y));
+
+            assertEquals(PngChunkType.tIME, dirs[4].getPngChunkType());
+            assertEquals("2013:01:01 04:08:30", dirs[4].getString(PngDirectory.TAG_LAST_MODIFICATION_TIME));
+
+            java.util.Date modTime = dirs[4].getDate(PngDirectory.TAG_LAST_MODIFICATION_TIME);
+            SimpleDateFormat formatter = new SimpleDateFormat("EE MMM DD HH:mm:ss z yyyy", Locale.US);
+            formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+            assertEquals("Tue Jan 01 04:08:30 GMT 2013", formatter.format(modTime));
+            assertNotNull(modTime);
+            assertEquals(1357013310000L, modTime.getTime());
+
+            assertEquals(PngChunkType.iTXt, dirs[5].getPngChunkType());
             @SuppressWarnings("unchecked")
-            List<KeyValuePair> pairs = (List<KeyValuePair>)directory.getObject(PngDirectory.TAG_TEXTUAL_DATA);
+            List<KeyValuePair> pairs = (List<KeyValuePair>)dirs[5].getObject(PngDirectory.TAG_TEXTUAL_DATA);
             assertNotNull(pairs);
             assertEquals(1, pairs.size());
-            assertEquals("Comment", pairs.get(0).getKey());
-            assertEquals("Created with GIMP", pairs.get(0).getValue());
+            assertEquals("Comment", pairs.get(0).getKey().toString());
+            assertEquals("Created with GIMP", pairs.get(0).getValue().toString());
         } finally {
             TimeZone.setDefault(timeZone);
         }
diff --git a/Tests/com/drew/lang/ByteArrayReaderTest.java b/Tests/com/drew/lang/ByteArrayReaderTest.java
index 858f2d7..01403c9 100644
--- a/Tests/com/drew/lang/ByteArrayReaderTest.java
+++ b/Tests/com/drew/lang/ByteArrayReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/lang/ByteConvertTest.java b/Tests/com/drew/lang/ByteConvertTest.java
new file mode 100644
index 0000000..dcf9f77
--- /dev/null
+++ b/Tests/com/drew/lang/ByteConvertTest.java
@@ -0,0 +1,25 @@
+package com.drew.lang;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ByteConvertTest
+{
+    @Test
+    public void toInt32BigEndian()
+    {
+        assertEquals(0x01020304, ByteConvert.toInt32BigEndian(new byte[]{1, 2, 3, 4}));
+        assertEquals(0x01020304, ByteConvert.toInt32BigEndian(new byte[]{1, 2, 3, 4, 5}));
+    }
+
+    @Test
+    public void toInt32LittleEndian()
+    {
+        assertEquals(0x04030201, ByteConvert.toInt32LittleEndian(new byte[]{1, 2, 3, 4}));
+        assertEquals(0x04030201, ByteConvert.toInt32LittleEndian(new byte[]{1, 2, 3, 4, 5}));
+    }
+}
diff --git a/Tests/com/drew/lang/ByteTrieTest.java b/Tests/com/drew/lang/ByteTrieTest.java
index 8b6cd32..701f360 100644
--- a/Tests/com/drew/lang/ByteTrieTest.java
+++ b/Tests/com/drew/lang/ByteTrieTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/lang/CompoundExceptionTest.java b/Tests/com/drew/lang/CompoundExceptionTest.java
index eb553c0..9b82de8 100644
--- a/Tests/com/drew/lang/CompoundExceptionTest.java
+++ b/Tests/com/drew/lang/CompoundExceptionTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/lang/GeoLocationTest.java b/Tests/com/drew/lang/GeoLocationTest.java
index 7a0807c..e51ea8c 100644
--- a/Tests/com/drew/lang/GeoLocationTest.java
+++ b/Tests/com/drew/lang/GeoLocationTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/lang/NullOutputStreamTest.java b/Tests/com/drew/lang/NullOutputStreamTest.java
index fdf88d7..d707388 100644
--- a/Tests/com/drew/lang/NullOutputStreamTest.java
+++ b/Tests/com/drew/lang/NullOutputStreamTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/lang/RandomAccessFileReaderTest.java b/Tests/com/drew/lang/RandomAccessFileReaderTest.java
index 1bc75c8..2b4ac15 100644
--- a/Tests/com/drew/lang/RandomAccessFileReaderTest.java
+++ b/Tests/com/drew/lang/RandomAccessFileReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
 
 package com.drew.lang;
 
+import com.drew.tools.FileUtil;
 import org.junit.After;
 import org.junit.Test;
 
@@ -48,9 +49,7 @@ public class RandomAccessFileReaderTest extends RandomAccessTestBase
             deleteTempFile();
 
             _tempFile = File.createTempFile("metadata-extractor-test-", ".tmp");
-            FileOutputStream stream = new FileOutputStream(_tempFile);
-            stream.write(bytes);
-            stream.close();
+            FileUtil.saveBytes(_tempFile, bytes);
             _randomAccessFile = new RandomAccessFile(_tempFile, "r");
             return new RandomAccessFileReader(_randomAccessFile);
         } catch (IOException e) {
diff --git a/Tests/com/drew/lang/RandomAccessStreamReaderTest.java b/Tests/com/drew/lang/RandomAccessStreamReaderTest.java
index 1f45da8..8295965 100644
--- a/Tests/com/drew/lang/RandomAccessStreamReaderTest.java
+++ b/Tests/com/drew/lang/RandomAccessStreamReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/lang/RandomAccessTestBase.java b/Tests/com/drew/lang/RandomAccessTestBase.java
index 9d93621..809c2e2 100644
--- a/Tests/com/drew/lang/RandomAccessTestBase.java
+++ b/Tests/com/drew/lang/RandomAccessTestBase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -246,19 +246,19 @@ public abstract class RandomAccessTestBase
         byte[] bytes = new byte[]{0x41, 0x42, 0x43, 0x44, 0x00, 0x45, 0x46, 0x47};
         RandomAccessReader reader = createReader(bytes);
 
-        assertEquals("", reader.getNullTerminatedString(0, 0));
-        assertEquals("A", reader.getNullTerminatedString(0, 1));
-        assertEquals("AB", reader.getNullTerminatedString(0, 2));
-        assertEquals("ABC", reader.getNullTerminatedString(0, 3));
-        assertEquals("ABCD", reader.getNullTerminatedString(0, 4));
-        assertEquals("ABCD", reader.getNullTerminatedString(0, 5));
-        assertEquals("ABCD", reader.getNullTerminatedString(0, 6));
+        assertEquals("", reader.getNullTerminatedString(0, 0, Charsets.UTF_8));
+        assertEquals("A", reader.getNullTerminatedString(0, 1, Charsets.UTF_8));
+        assertEquals("AB", reader.getNullTerminatedString(0, 2, Charsets.UTF_8));
+        assertEquals("ABC", reader.getNullTerminatedString(0, 3, Charsets.UTF_8));
+        assertEquals("ABCD", reader.getNullTerminatedString(0, 4, Charsets.UTF_8));
+        assertEquals("ABCD", reader.getNullTerminatedString(0, 5, Charsets.UTF_8));
+        assertEquals("ABCD", reader.getNullTerminatedString(0, 6, Charsets.UTF_8));
 
-        assertEquals("BCD", reader.getNullTerminatedString(1, 3));
-        assertEquals("BCD", reader.getNullTerminatedString(1, 4));
-        assertEquals("BCD", reader.getNullTerminatedString(1, 5));
+        assertEquals("BCD", reader.getNullTerminatedString(1, 3, Charsets.UTF_8));
+        assertEquals("BCD", reader.getNullTerminatedString(1, 4, Charsets.UTF_8));
+        assertEquals("BCD", reader.getNullTerminatedString(1, 5, Charsets.UTF_8));
 
-        assertEquals("", reader.getNullTerminatedString(4, 3));
+        assertEquals("", reader.getNullTerminatedString(4, 3, Charsets.UTF_8));
     }
 
     @Test
@@ -267,19 +267,19 @@ public abstract class RandomAccessTestBase
         byte[] bytes = new byte[]{0x41, 0x42, 0x43, 0x44, 0x00, 0x45, 0x46, 0x47};
         RandomAccessReader reader = createReader(bytes);
 
-        assertEquals("", reader.getString(0, 0));
-        assertEquals("A", reader.getString(0, 1));
-        assertEquals("AB", reader.getString(0, 2));
-        assertEquals("ABC", reader.getString(0, 3));
-        assertEquals("ABCD", reader.getString(0, 4));
-        assertEquals("ABCD\0", reader.getString(0, 5));
-        assertEquals("ABCD\0E", reader.getString(0, 6));
+        assertEquals("", reader.getString(0, 0, Charsets.UTF_8));
+        assertEquals("A", reader.getString(0, 1, Charsets.UTF_8));
+        assertEquals("AB", reader.getString(0, 2, Charsets.UTF_8));
+        assertEquals("ABC", reader.getString(0, 3, Charsets.UTF_8));
+        assertEquals("ABCD", reader.getString(0, 4, Charsets.UTF_8));
+        assertEquals("ABCD\0", reader.getString(0, 5, Charsets.UTF_8));
+        assertEquals("ABCD\0E", reader.getString(0, 6, Charsets.UTF_8));
 
-        assertEquals("BCD", reader.getString(1, 3));
-        assertEquals("BCD\0", reader.getString(1, 4));
-        assertEquals("BCD\0E", reader.getString(1, 5));
+        assertEquals("BCD", reader.getString(1, 3, Charsets.UTF_8));
+        assertEquals("BCD\0", reader.getString(1, 4, Charsets.UTF_8));
+        assertEquals("BCD\0E", reader.getString(1, 5, Charsets.UTF_8));
 
-        assertEquals("\0EF", reader.getString(4, 3));
+        assertEquals("\0EF", reader.getString(4, 3, Charsets.UTF_8));
     }
 
     @Test
diff --git a/Tests/com/drew/lang/RationalTest.java b/Tests/com/drew/lang/RationalTest.java
index ad68290..c92da46 100644
--- a/Tests/com/drew/lang/RationalTest.java
+++ b/Tests/com/drew/lang/RationalTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -30,6 +30,20 @@ import static org.junit.Assert.assertTrue;
  */
 public class RationalTest
 {
+    @Test
+    public void testCompare() throws Exception
+    {
+        Rational third1 = new Rational(1, 3);
+        Rational third2 = new Rational(2, 6);
+        assertEquals(0, third1.compareTo(third2));
+
+        Rational half = new Rational(1, 2);
+        assertEquals(-1, third1.compareTo(half));
+
+        Rational negForth = new Rational(-1, 4);
+        assertEquals(1, third1.compareTo(negForth));
+    }
+
     @Test
     public void testCreateRational() throws Exception
     {
@@ -105,4 +119,126 @@ public class RationalTest
         assertEquals(0L, new Rational(0, 0).longValue());
         assertTrue(new Rational(0, 0).isInteger());
     }
+
+    private static final int[] _primes =
+    {
+        2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131,
+        137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271,
+        277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433,
+        439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601,
+        607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769,
+        773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953,
+        967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103,
+        1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279,
+        1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447,
+        1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597,
+        1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753,
+        1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933,
+        1949, 1951, 1973, 1979, 1987, 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099,
+        2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281,
+        2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423, 2437,
+        2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647,
+        2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, 2789,
+        2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909, 2917, 2927, 2939, 2953, 2957, 2963,
+        2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169,
+        3181, 3187, 3191, 3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343,
+        3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527, 3529,
+        3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691,
+        3697, 3701, 3709, 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877,
+        3881, 3889, 3907, 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051,
+        4057, 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231, 4241,
+        4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, 4421, 4423, 4441,
+        4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621, 4637,
+        4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799, 4801,
+        4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999,
+        5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189,
+        5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399, 5407,
+        5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569,
+        5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749,
+        5779, 5783, 5791, 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927,
+        5939, 5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133, 6143,
+        6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301, 6311, 6317, 6323,
+        6329, 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473, 6481, 6491, 6521, 6529, 6547,
+        6551, 6553, 6563, 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691, 6701, 6703, 6709, 6719,
+        6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911,
+        6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997, 7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109,
+        7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297, 7307, 7309, 7321,
+        7331, 7333, 7349, 7351, 7369, 7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, 7523, 7529, 7537,
+        7541, 7547, 7549, 7559, 7561, 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699,
+        7703, 7717, 7723, 7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907,
+        7919, 7927, 7933, 7937, 7949, 7951, 7963, 7993, 8009, 8011, 8017, 8039, 8053, 8059, 8069, 8081, 8087, 8089, 8093, 8101, 8111, 8117,
+        8123, 8147, 8161, 8167, 8171, 8179, 8191, 8209, 8219, 8221, 8231, 8233, 8237, 8243, 8263, 8269, 8273, 8287, 8291, 8293, 8297, 8311,
+        8317, 8329, 8353, 8363, 8369, 8377, 8387, 8389, 8419, 8423, 8429, 8431, 8443, 8447, 8461, 8467, 8501, 8513, 8521, 8527, 8537, 8539,
+        8543, 8563, 8573, 8581, 8597, 8599, 8609, 8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677, 8681, 8689, 8693, 8699, 8707, 8713, 8719,
+        8731, 8737, 8741, 8747, 8753, 8761, 8779, 8783, 8803, 8807, 8819, 8821, 8831, 8837, 8839, 8849, 8861, 8863, 8867, 8887, 8893, 8923,
+        8929, 8933, 8941, 8951, 8963, 8969, 8971, 8999, 9001, 9007, 9011, 9013, 9029, 9041, 9043, 9049, 9059, 9067, 9091, 9103, 9109, 9127,
+        9133, 9137, 9151, 9157, 9161, 9173, 9181, 9187, 9199, 9203, 9209, 9221, 9227, 9239, 9241, 9257, 9277, 9281, 9283, 9293, 9311, 9319,
+        9323, 9337, 9341, 9343, 9349, 9371, 9377, 9391, 9397, 9403, 9413, 9419, 9421, 9431, 9433, 9437, 9439, 9461, 9463, 9467, 9473, 9479,
+        9491, 9497, 9511, 9521, 9533, 9539, 9547, 9551, 9587, 9601, 9613, 9619, 9623, 9629, 9631, 9643, 9649, 9661, 9677, 9679, 9689, 9697,
+        9719, 9721, 9733, 9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811, 9817, 9829, 9833, 9839, 9851, 9857, 9859, 9871, 9883,
+        9887, 9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967, 9973
+    };
+
+    @Test
+    public void simplifiedInstances()
+    {
+        Rational simple = new Rational(1, 2);
+
+        for (int prime : _primes)
+        {
+            Rational complex = new Rational(prime, 2 * prime);
+            Rational actualSimple = complex.getSimplifiedInstance();
+
+            assertTrue(simple.equalsExact(actualSimple));
+            assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001);
+        }
+
+        simple = new Rational(2, 1);
+
+        for (int prime : _primes)
+        {
+            Rational complex = new Rational(2 * prime, prime);
+            Rational actualSimple = complex.getSimplifiedInstance();
+
+            assertTrue(simple.equalsExact(actualSimple));
+            assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001);
+        }
+
+        simple = new Rational(-1, 2);
+
+        for (int prime : _primes)
+        {
+            Rational complex = new Rational(-prime, 2 * prime);
+            Rational actualSimple = complex.getSimplifiedInstance();
+
+            assertTrue(simple.equalsExact(actualSimple));
+            assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001);
+        }
+
+        simple = new Rational(1, -2);
+
+        for (int prime : _primes)
+        {
+            Rational complex = new Rational(prime, -2 * prime);
+            Rational actualSimple = complex.getSimplifiedInstance();
+
+            assertTrue(simple.equalsExact(actualSimple));
+            assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001);
+        }
+
+        simple = new Rational(-1, -2);
+
+        for (int prime : _primes)
+        {
+            Rational complex = new Rational(-prime, -2 * prime);
+            Rational actualSimple = complex.getSimplifiedInstance();
+
+            assertTrue(simple.equalsExact(actualSimple));
+            assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001);
+        }
+
+        assertEquals(new Rational(-32768, 65535), new Rational(-32768, 65535).getSimplifiedInstance());
+        assertEquals(new Rational(-32768, 32767), new Rational(-32768, 32767).getSimplifiedInstance());
+    }
+
 }
diff --git a/Tests/com/drew/lang/SequentialAccessTestBase.java b/Tests/com/drew/lang/SequentialAccessTestBase.java
index ba37e32..57e4b5e 100644
--- a/Tests/com/drew/lang/SequentialAccessTestBase.java
+++ b/Tests/com/drew/lang/SequentialAccessTestBase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -231,13 +231,13 @@ public abstract class SequentialAccessTestBase
 
         // Test max length
         for (int i = 0; i < bytes.length; i++) {
-            assertEquals("ABCDEFG".substring(0, i), createReader(bytes).getNullTerminatedString(i));
+            assertEquals("ABCDEFG".substring(0, i), createReader(bytes).getNullTerminatedString(i, Charsets.UTF_8));
         }
 
-        assertEquals("", createReader(new byte[]{0}).getNullTerminatedString(10));
-        assertEquals("A", createReader(new byte[]{0x41, 0}).getNullTerminatedString(10));
-        assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0}).getNullTerminatedString(10));
-        assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0, 0x43}).getNullTerminatedString(10));
+        assertEquals("", createReader(new byte[]{0}).getNullTerminatedString(10, Charsets.UTF_8));
+        assertEquals("A", createReader(new byte[]{0x41, 0}).getNullTerminatedString(10, Charsets.UTF_8));
+        assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0}).getNullTerminatedString(10, Charsets.UTF_8));
+        assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0, 0x43}).getNullTerminatedString(10, Charsets.UTF_8));
     }
 
     @Test
diff --git a/Tests/com/drew/lang/SequentialByteArrayReaderTest.java b/Tests/com/drew/lang/SequentialByteArrayReaderTest.java
index 66b8a2c..44ab504 100644
--- a/Tests/com/drew/lang/SequentialByteArrayReaderTest.java
+++ b/Tests/com/drew/lang/SequentialByteArrayReaderTest.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.lang;
 
 import org.junit.Test;
diff --git a/Tests/com/drew/lang/StreamReaderTest.java b/Tests/com/drew/lang/StreamReaderTest.java
index 52e18e5..537a888 100644
--- a/Tests/com/drew/lang/StreamReaderTest.java
+++ b/Tests/com/drew/lang/StreamReaderTest.java
@@ -1,3 +1,23 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.lang;
 
 import org.junit.Test;
diff --git a/Tests/com/drew/lang/StringUtilTest.java b/Tests/com/drew/lang/StringUtilTest.java
index 3bd3870..bd91cc4 100644
--- a/Tests/com/drew/lang/StringUtilTest.java
+++ b/Tests/com/drew/lang/StringUtilTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/AgeTest.java b/Tests/com/drew/metadata/AgeTest.java
index 22fcb7c..7cd49c1 100644
--- a/Tests/com/drew/metadata/AgeTest.java
+++ b/Tests/com/drew/metadata/AgeTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/DirectoryTest.java b/Tests/com/drew/metadata/DirectoryTest.java
index a4d35b8..b638dc0 100644
--- a/Tests/com/drew/metadata/DirectoryTest.java
+++ b/Tests/com/drew/metadata/DirectoryTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -26,7 +26,9 @@ import com.drew.metadata.exif.ExifSubIFDDirectory;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Calendar;
 import java.util.GregorianCalendar;
+import java.util.TimeZone;
 
 import static org.junit.Assert.*;
 
@@ -105,19 +107,88 @@ public class DirectoryTest
     @Test
     public void testSetStringAndGetDate() throws Exception
     {
-        String date1 = "2002:01:30 24:59:59";
-        String date2 = "2002:01:30 24:59";
-        String date3 = "2002-01-30 24:59:59";
-        String date4 = "2002-01-30 24:59";
+        String date1 = "2002:01:30 23:59:59";
+        String date2 = "2002:01:30 23:59";
+        String date3 = "2002-01-30 23:59:59";
+        String date4 = "2002-01-30 23:59";
+        String date5 = "2002-01-30T23:59:59.099-08:00";
+        String date6 = "2002-01-30T23:59:59.099";
+        String date7 = "2002-01-30T23:59:59-08:00";
+        String date8 = "2002-01-30T23:59:59";
+        String date9 = "2002-01-30T23:59-08:00";
+        String date10 = "2002-01-30T23:59";
+        String date11 = "2002-01-30";
+        String date12 = "2002-01";
+        String date13 = "2002";
         _directory.setString(1, date1);
         _directory.setString(2, date2);
         _directory.setString(3, date3);
         _directory.setString(4, date4);
+        _directory.setString(5, date5);
+        _directory.setString(6, date6);
+        _directory.setString(7, date7);
+        _directory.setString(8, date8);
+        _directory.setString(9, date9);
+        _directory.setString(10, date10);
+        _directory.setString(11, date11);
+        _directory.setString(12, date12);
+        _directory.setString(13, date13);
         assertEquals(date1, _directory.getString(1));
-        assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 59).getTime(), _directory.getDate(1));
-        assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 0).getTime(), _directory.getDate(2));
-        assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 59).getTime(), _directory.getDate(3));
-        assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 0).getTime(), _directory.getDate(4));
+
+            // Don't use default timezone
+        TimeZone gmt = TimeZone.getTimeZone("GMT");
+        GregorianCalendar gc = new GregorianCalendar(gmt);
+            // clear millis to 0 or test will fail
+        gc.setTimeInMillis(0);
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 59);
+        assertEquals(gc.getTime(), _directory.getDate(1, null));
+
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 0);
+        assertEquals(gc.getTime(), _directory.getDate(2, null));
+
+            // Use specific timezone
+        TimeZone pst = TimeZone.getTimeZone("PST");
+        gc = new GregorianCalendar(pst);
+        gc.setTimeInMillis(0);
+
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 59);
+        assertEquals(gc.getTime(), _directory.getDate(3, pst));
+
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 0);
+        assertEquals(gc.getTime(), _directory.getDate(4, pst));
+
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 59);
+        gc.set(Calendar.MILLISECOND, 99);
+        assertEquals(gc.getTime(), _directory.getDate(5, null));
+        assertEquals(gc.getTime(), _directory.getDate(5, gmt));
+        assertEquals(gc.getTime(), _directory.getDate(6, pst));
+
+        assertEquals(gc.getTime(), _directory.getDate(5, "011", null));
+        assertEquals(gc.getTime(), _directory.getDate(6, "011", pst));
+        assertEquals(gc.getTime(), _directory.getDate(7, "099", null));
+        assertEquals(gc.getTime(), _directory.getDate(8, "099", pst));
+
+        gc.set(Calendar.MILLISECOND, 0);
+        assertEquals(gc.getTime(), _directory.getDate(7, null));
+        assertEquals(gc.getTime(), _directory.getDate(7, gmt));
+        assertEquals(gc.getTime(), _directory.getDate(8, pst));
+
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 0);
+        assertEquals(gc.getTime(), _directory.getDate(9, null));
+        assertEquals(gc.getTime(), _directory.getDate(9, gmt));
+        assertEquals(gc.getTime(), _directory.getDate(10, pst));
+
+        gc = new GregorianCalendar(gmt);
+        gc.setTimeInMillis(0);
+
+        gc.set(2002, GregorianCalendar.JANUARY, 30, 0, 0, 0);
+        assertEquals(gc.getTime(), _directory.getDate(11, null));
+
+        gc.set(2002, GregorianCalendar.JANUARY, 1, 0, 0, 0);
+        assertEquals(gc.getTime(), _directory.getDate(12, null));
+
+        gc.set(2002, GregorianCalendar.JANUARY, 1, 0, 0, 0);
+        assertEquals(gc.getTime(), _directory.getDate(13, null));
     }
 
     @Test
diff --git a/Tests/com/drew/metadata/MetadataTest.java b/Tests/com/drew/metadata/MetadataTest.java
index 0cc98d7..2535641 100644
--- a/Tests/com/drew/metadata/MetadataTest.java
+++ b/Tests/com/drew/metadata/MetadataTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,9 +22,13 @@ package com.drew.metadata;
 
 import com.drew.metadata.exif.ExifIFD0Directory;
 import com.drew.metadata.exif.ExifSubIFDDirectory;
-import com.drew.metadata.iptc.IptcDirectory;
+import com.drew.metadata.exif.ExifThumbnailDirectory;
 import org.junit.Test;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
 import static org.junit.Assert.*;
 
 /**
@@ -34,59 +38,79 @@ import static org.junit.Assert.*;
  */
 public class MetadataTest
 {
-    @Test public void testGetDirectoryWhenNotExists()
+    @Test
+    public void testGetDirectoryWhenNotExists()
     {
-        assertNull(new Metadata().getDirectory(ExifSubIFDDirectory.class));
+        assertNull(new Metadata().getFirstDirectoryOfType(ExifSubIFDDirectory.class));
     }
 
-    @Test public void testGetOrCreateDirectoryWhenNotExists()
+    @Test
+    public void testHasErrors() throws Exception
     {
-        assertNotNull(new Metadata().getOrCreateDirectory(ExifSubIFDDirectory.class));
-    }
+        ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
+        directory.addError("Test Error 1");
 
-    @Test public void testGetDirectoryReturnsSameInstance()
-    {
         Metadata metadata = new Metadata();
-        Directory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
-        assertSame(directory, metadata.getDirectory(ExifSubIFDDirectory.class));
-    }
+        assertFalse(metadata.hasErrors());
 
-    @Test public void testGetOrCreateDirectoryReturnsSameInstance()
-    {
-        Metadata metadata = new Metadata();
-        Directory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
-        assertSame(directory, metadata.getOrCreateDirectory(ExifSubIFDDirectory.class));
-        assertNotSame(directory, metadata.getOrCreateDirectory(IptcDirectory.class));
+        metadata.addDirectory(directory);
+        assertTrue(metadata.hasErrors());
     }
 
     @Test
-    public void testHasErrors() throws Exception
+    public void testToString()
     {
         Metadata metadata = new Metadata();
-        assertFalse(metadata.hasErrors());
-        final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
-        directory.addError("Test Error 1");
-        assertTrue(metadata.hasErrors());
+        assertEquals("Metadata (0 directories)", metadata.toString());
+
+        metadata.addDirectory(new ExifIFD0Directory());
+        assertEquals("Metadata (1 directory)", metadata.toString());
+
+        metadata.addDirectory(new ExifSubIFDDirectory());
+        assertEquals("Metadata (2 directories)", metadata.toString());
     }
 
     @Test
-    public void testGetErrors() throws Exception
+    public void testOrderOfSameType()
     {
         Metadata metadata = new Metadata();
-        assertFalse(metadata.hasErrors());
-        final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
-        directory.addError("Test Error 1");
-        assertTrue(metadata.hasErrors());
+        Directory directory2 = new ExifSubIFDDirectory();
+        Directory directory3 = new ExifSubIFDDirectory();
+        Directory directory1 = new ExifSubIFDDirectory();
+
+        metadata.addDirectory(directory1);
+        metadata.addDirectory(directory2);
+        metadata.addDirectory(directory3);
+
+        Collection<ExifSubIFDDirectory> directories = metadata.getDirectoriesOfType(ExifSubIFDDirectory.class);
+
+        assertNotNull(directories);
+        assertEquals(3, directories.size());
+        assertSame(directory1, directories.toArray()[0]);
+        assertSame(directory2, directories.toArray()[1]);
+        assertSame(directory3, directories.toArray()[2]);
     }
 
     @Test
-    public void testToString()
+    public void testOrderOfDifferentTypes()
     {
         Metadata metadata = new Metadata();
-        assertEquals("Metadata (0 directories)", metadata.toString());
-        metadata.getOrCreateDirectory(ExifIFD0Directory.class);
-        assertEquals("Metadata (1 directory)", metadata.toString());
-        metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
-        assertEquals("Metadata (2 directories)", metadata.toString());
+        Directory directory1 = new ExifSubIFDDirectory();
+        Directory directory2 = new ExifThumbnailDirectory();
+        Directory directory3 = new ExifIFD0Directory();
+
+        metadata.addDirectory(directory1);
+        metadata.addDirectory(directory2);
+        metadata.addDirectory(directory3);
+
+        List<Directory> directories = new ArrayList<Directory>();
+        for (Directory directory : metadata.getDirectories()) {
+            directories.add(directory);
+        }
+
+        assertEquals(3, directories.size());
+        assertSame(directory1, directories.toArray()[0]);
+        assertSame(directory2, directories.toArray()[1]);
+        assertSame(directory3, directories.toArray()[2]);
     }
 }
diff --git a/Tests/com/drew/metadata/MockDirectory.java b/Tests/com/drew/metadata/MockDirectory.java
index eb88b70..61541dd 100644
--- a/Tests/com/drew/metadata/MockDirectory.java
+++ b/Tests/com/drew/metadata/MockDirectory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java b/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java
index 58cd670..d3b1dfe 100644
--- a/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java
+++ b/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -44,7 +44,7 @@ public class AdobeJpegReaderTest
         Metadata metadata = new Metadata();
         new AdobeJpegReader().extract(new SequentialByteArrayReader(FileUtil.readBytes(filePath)), metadata);
 
-        AdobeJpegDirectory directory = metadata.getDirectory(AdobeJpegDirectory.class);
+        AdobeJpegDirectory directory = metadata.getFirstDirectoryOfType(AdobeJpegDirectory.class);
         assertNotNull(directory);
         return directory;
     }
diff --git a/Tests/com/drew/metadata/bmp/BmpReaderTest.java b/Tests/com/drew/metadata/bmp/BmpReaderTest.java
index 5478045..10fc40e 100644
--- a/Tests/com/drew/metadata/bmp/BmpReaderTest.java
+++ b/Tests/com/drew/metadata/bmp/BmpReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ public class BmpReaderTest
         new BmpReader().extract(new StreamReader(stream), metadata);
         stream.close();
 
-        BmpHeaderDirectory directory = metadata.getDirectory(BmpHeaderDirectory.class);
+        BmpHeaderDirectory directory = metadata.getFirstDirectoryOfType(BmpHeaderDirectory.class);
         assertNotNull(directory);
         return directory;
     }
diff --git a/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java b/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java
index f447ddb..c81fb57 100644
--- a/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java
+++ b/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import com.drew.metadata.exif.makernotes.CanonMakernoteDescriptor;
 import com.drew.metadata.exif.makernotes.CanonMakernoteDirectory;
 import org.junit.Test;
 
+import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*;
 import static org.junit.Assert.assertEquals;
 
 /**
@@ -39,34 +40,34 @@ public class CanonMakernoteDescriptorTest
 
         // set and check values
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0xFFC0);
-        assertEquals("-2.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0xFFC0);
+        assertEquals("-2.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0xffd4);
-        assertEquals("-1.375 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0xffd4);
+        assertEquals("-1.375 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0000);
-        assertEquals("0.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0000);
+        assertEquals("0.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x000c);
-        assertEquals("0.375 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x000c);
+        assertEquals("0.375 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0010);
-        assertEquals("0.5 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0010);
+        assertEquals("0.5 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0014);
-        assertEquals("0.625 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0014);
+        assertEquals("0.625 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0020);
-        assertEquals("1.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0020);
+        assertEquals("1.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0030);
-        assertEquals("1.5 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0030);
+        assertEquals("1.5 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0034);
-        assertEquals("1.625 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0034);
+        assertEquals("1.625 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
 
-        directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0040);
-        assertEquals("2.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS));
+        directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0040);
+        assertEquals("2.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS));
     }
 }
diff --git a/Tests/com/drew/metadata/exif/ExifDirectoryTest.java b/Tests/com/drew/metadata/exif/ExifDirectoryTest.java
index f70fbea..5c44662 100644
--- a/Tests/com/drew/metadata/exif/ExifDirectoryTest.java
+++ b/Tests/com/drew/metadata/exif/ExifDirectoryTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,23 +21,24 @@
 package com.drew.metadata.exif;
 
 import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.jpeg.JpegSegmentReader;
-import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.GeoLocation;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.MetadataException;
 import org.junit.Test;
 
-import java.io.File;
 import java.io.IOException;
+import java.util.TimeZone;
 
 import static org.junit.Assert.*;
 
 /**
- * Unit tests for {@link ExifSubIFDDirectory}, {@link ExifIFD0Directory}, {@link ExifThumbnailDirectory}.
+ * Unit tests for {@link ExifSubIFDDirectory}, {@link ExifIFD0Directory}, {@link ExifThumbnailDirectory} and
+ * {@link GpsDirectory}.
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("ConstantConditions")
 public class ExifDirectoryTest
 {
     @Test
@@ -46,73 +47,88 @@ public class ExifDirectoryTest
         Directory subIFDDirectory = new ExifSubIFDDirectory();
         Directory ifd0Directory = new ExifIFD0Directory();
         Directory thumbDirectory = new ExifThumbnailDirectory();
+        Directory gpsDirectory = new GpsDirectory();
 
         assertFalse(subIFDDirectory.hasErrors());
         assertFalse(ifd0Directory.hasErrors());
         assertFalse(thumbDirectory.hasErrors());
+        assertFalse(gpsDirectory.hasErrors());
 
         assertEquals("Exif IFD0", ifd0Directory.getName());
         assertEquals("Exif SubIFD", subIFDDirectory.getName());
         assertEquals("Exif Thumbnail", thumbDirectory.getName());
+        assertEquals("GPS", gpsDirectory.getName());
     }
 
     @Test
-    public void testGetThumbnailData() throws Exception
+    public void testDateTime() throws JpegProcessingException, IOException, MetadataException
     {
-        ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/withExif.jpg.app1", ExifThumbnailDirectory.class);
-
-        byte[] thumbData = directory.getThumbnailData();
-        assertNotNull(thumbData);
-        try {
-            // attempt to read the thumbnail -- it should be a legal Jpeg file
-            JpegSegmentReader.readSegments(new SequentialByteArrayReader(thumbData), null);
-        } catch (JpegProcessingException e) {
-            fail("Unable to construct JpegSegmentReader from thumbnail data");
-        }
-    }
+        Metadata metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType2a.jpg.app1");
 
-    @Test
-    public void testWriteThumbnail() throws Exception
-    {
-        ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/manuallyAddedThumbnail.jpg.app1", ExifThumbnailDirectory.class);
-
-        assertTrue(directory.hasThumbnailData());
-
-        File thumbnailFile = File.createTempFile("thumbnail", ".jpg");
-        try {
-            directory.writeThumbnail(thumbnailFile.getAbsolutePath());
-            File file = new File(thumbnailFile.getAbsolutePath());
-            assertEquals(2970, file.length());
-            assertTrue(file.exists());
-        } finally {
-            if (!thumbnailFile.delete())
-                fail("Unable to delete temp thumbnail file.");
-        }
-    }
+        ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+        ExifSubIFDDirectory exifSubIFDDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
 
-//    @Test
-//    public void testContainsThumbnail()
-//    {
-//        ExifSubIFDDirectory exifDirectory = new ExifSubIFDDirectory();
-//
-//        assertTrue(!exifDirectory.hasThumbnailData());
-//
-//        exifDirectory.setObject(ExifSubIFDDirectory.TAG_THUMBNAIL_DATA, "foo");
-//
-//        assertTrue(exifDirectory.hasThumbnailData());
-//    }
+        assertNotNull(exifIFD0Directory);
+        assertNotNull(exifSubIFDDirectory);
+
+        assertEquals("2003:10:15 10:37:08", exifIFD0Directory.getString(ExifIFD0Directory.TAG_DATETIME));
+        assertEquals("80", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME));
+        assertEquals("2003:10:15 10:37:08", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL));
+        assertEquals("80", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL));
+        assertEquals("2003:10:15 10:37:08", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED));
+        assertEquals("80", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED));
+
+        assertEquals(1066214228800L, exifIFD0Directory.getDate(
+            ExifIFD0Directory.TAG_DATETIME,
+            exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME),
+            null
+        ).getTime());
+        assertEquals(1066210628800L, exifIFD0Directory.getDate(
+            ExifIFD0Directory.TAG_DATETIME,
+            exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME),
+            TimeZone.getTimeZone("GMT+0100")
+        ).getTime());
+        assertEquals(1066214228800L, exifSubIFDDirectory.getDateOriginal().getTime());
+        assertEquals(1066210628800L, exifSubIFDDirectory.getDateOriginal(TimeZone.getTimeZone("GMT+0100")).getTime());
+        assertEquals(1066214228800L, exifSubIFDDirectory.getDateDigitized().getTime());
+        assertEquals(1066210628800L, exifSubIFDDirectory.getDateDigitized(TimeZone.getTimeZone("GMT+0100")).getTime());
+    }
 
     @Test
     public void testResolution() throws JpegProcessingException, IOException, MetadataException
     {
         Metadata metadata = ExifReaderTest.processBytes("Tests/Data/withUncompressedRGBThumbnail.jpg.app1");
 
-        ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
+        ExifThumbnailDirectory thumbnailDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
         assertNotNull(thumbnailDirectory);
         assertEquals(72, thumbnailDirectory.getInt(ExifThumbnailDirectory.TAG_X_RESOLUTION));
 
-        ExifIFD0Directory exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class);
+        ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
         assertNotNull(exifIFD0Directory);
         assertEquals(216, exifIFD0Directory.getInt(ExifIFD0Directory.TAG_X_RESOLUTION));
     }
+
+    @Test
+    public void testGeoLocation() throws IOException, MetadataException
+    {
+        Metadata metadata = ExifReaderTest.processBytes("Tests/Data/withExifAndIptc.jpg.app1.0");
+
+        GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class);
+        assertNotNull(gpsDirectory);
+        GeoLocation geoLocation = gpsDirectory.getGeoLocation();
+        assertEquals(54.989666666666665, geoLocation.getLatitude(), 0.001);
+        assertEquals(-1.9141666666666666, geoLocation.getLongitude(), 0.001);
+    }
+
+    @Test
+    public void testGpsDate() throws IOException, MetadataException
+    {
+        Metadata metadata = ExifReaderTest.processBytes("Tests/Data/withPanasonicFaces.jpg.app1");
+
+        GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class);
+        assertNotNull(gpsDirectory);
+        assertEquals("2010:06:24", gpsDirectory.getString(GpsDirectory.TAG_DATE_STAMP));
+        assertEquals("10/1 17/1 21/1", gpsDirectory.getString(GpsDirectory.TAG_TIME_STAMP));
+        assertEquals(1277374641000L, gpsDirectory.getGpsDate().getTime());
+    }
 }
diff --git a/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java b/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java
index 1a9101e..8b1ca9a 100644
--- a/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java
+++ b/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ package com.drew.metadata.exif;
 import com.drew.lang.Rational;
 import org.junit.Test;
 
+import static com.drew.metadata.exif.ExifIFD0Directory.*;
 import static org.junit.Assert.assertEquals;
 
 /**
@@ -37,22 +38,22 @@ public class ExifIFD0DescriptorTest
     public void testXResolutionDescription() throws Exception
     {
         ExifIFD0Directory directory = new ExifIFD0Directory();
-        directory.setRational(ExifIFD0Directory.TAG_X_RESOLUTION, new Rational(72, 1));
+        directory.setRational(TAG_X_RESOLUTION, new Rational(72, 1));
         // 2 is for 'Inch'
-        directory.setInt(ExifIFD0Directory.TAG_RESOLUTION_UNIT, 2);
+        directory.setInt(TAG_RESOLUTION_UNIT, 2);
         ExifIFD0Descriptor descriptor = new ExifIFD0Descriptor(directory);
-        assertEquals("72 dots per inch", descriptor.getDescription(ExifIFD0Directory.TAG_X_RESOLUTION));
+        assertEquals("72 dots per inch", descriptor.getDescription(TAG_X_RESOLUTION));
     }
 
     @Test
     public void testYResolutionDescription() throws Exception
     {
         ExifIFD0Directory directory = new ExifIFD0Directory();
-        directory.setRational(ExifIFD0Directory.TAG_Y_RESOLUTION, new Rational(50, 1));
+        directory.setRational(TAG_Y_RESOLUTION, new Rational(50, 1));
         // 3 is for 'cm'
-        directory.setInt(ExifIFD0Directory.TAG_RESOLUTION_UNIT, 3);
+        directory.setInt(TAG_RESOLUTION_UNIT, 3);
         ExifIFD0Descriptor descriptor = new ExifIFD0Descriptor(directory);
-        assertEquals("50 dots per cm", descriptor.getDescription(ExifIFD0Directory.TAG_Y_RESOLUTION));
+        assertEquals("50 dots per cm", descriptor.getDescription(TAG_Y_RESOLUTION));
     }
 
     @Test
@@ -60,17 +61,17 @@ public class ExifIFD0DescriptorTest
     {
         ExifIFD0Directory directory = ExifReaderTest.processBytes("Tests/Data/windowsXpFields.jpg.app1", ExifIFD0Directory.class);
 
-        assertEquals("Testing artist\0", directory.getString(ExifIFD0Directory.TAG_WIN_AUTHOR, "UTF-16LE"));
-        assertEquals("Testing comments\0", directory.getString(ExifIFD0Directory.TAG_WIN_COMMENT, "UTF-16LE"));
-        assertEquals("Testing keywords\0", directory.getString(ExifIFD0Directory.TAG_WIN_KEYWORDS, "UTF-16LE"));
-        assertEquals("Testing subject\0", directory.getString(ExifIFD0Directory.TAG_WIN_SUBJECT, "UTF-16LE"));
-        assertEquals("Testing title\0", directory.getString(ExifIFD0Directory.TAG_WIN_TITLE, "UTF-16LE"));
+        assertEquals("Testing artist\0", directory.getString(TAG_WIN_AUTHOR, "UTF-16LE"));
+        assertEquals("Testing comments\0", directory.getString(TAG_WIN_COMMENT, "UTF-16LE"));
+        assertEquals("Testing keywords\0", directory.getString(TAG_WIN_KEYWORDS, "UTF-16LE"));
+        assertEquals("Testing subject\0", directory.getString(TAG_WIN_SUBJECT, "UTF-16LE"));
+        assertEquals("Testing title\0", directory.getString(TAG_WIN_TITLE, "UTF-16LE"));
 
         ExifIFD0Descriptor descriptor = new ExifIFD0Descriptor(directory);
-        assertEquals("Testing artist", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_AUTHOR));
-        assertEquals("Testing comments", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_COMMENT));
-        assertEquals("Testing keywords", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_KEYWORDS));
-        assertEquals("Testing subject", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_SUBJECT));
-        assertEquals("Testing title", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_TITLE));
+        assertEquals("Testing artist", descriptor.getDescription(TAG_WIN_AUTHOR));
+        assertEquals("Testing comments", descriptor.getDescription(TAG_WIN_COMMENT));
+        assertEquals("Testing keywords", descriptor.getDescription(TAG_WIN_KEYWORDS));
+        assertEquals("Testing subject", descriptor.getDescription(TAG_WIN_SUBJECT));
+        assertEquals("Testing title", descriptor.getDescription(TAG_WIN_TITLE));
     }
 }
diff --git a/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java b/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java
index 797f8fd..8bb07fb 100644
--- a/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java
+++ b/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ package com.drew.metadata.exif;
 
 import org.junit.Test;
 
+import static com.drew.metadata.exif.ExifInteropDirectory.*;
 import static org.junit.Assert.assertEquals;
 
 /**
@@ -36,9 +37,9 @@ public class ExifInteropDescriptorTest
     public void testGetInteropVersionDescription() throws Exception
     {
         ExifInteropDirectory directory = new ExifInteropDirectory();
-        directory.setIntArray(ExifInteropDirectory.TAG_INTEROP_VERSION, new int[]{0, 1, 0, 0});
+        directory.setIntArray(TAG_INTEROP_VERSION, new int[]{0, 1, 0, 0});
         ExifInteropDescriptor descriptor = new ExifInteropDescriptor(directory);
-        assertEquals("1.00", descriptor.getDescription(ExifInteropDirectory.TAG_INTEROP_VERSION));
+        assertEquals("1.00", descriptor.getDescription(TAG_INTEROP_VERSION));
         assertEquals("1.00", descriptor.getInteropVersionDescription());
     }
 
@@ -46,9 +47,9 @@ public class ExifInteropDescriptorTest
     public void testGetInteropIndexDescription() throws Exception
     {
         ExifInteropDirectory directory = new ExifInteropDirectory();
-        directory.setString(ExifInteropDirectory.TAG_INTEROP_INDEX, "R98");
+        directory.setString(TAG_INTEROP_INDEX, "R98");
         ExifInteropDescriptor descriptor = new ExifInteropDescriptor(directory);
-        assertEquals("Recommended Exif Interoperability Rules (ExifR98)", descriptor.getDescription(ExifInteropDirectory.TAG_INTEROP_INDEX));
+        assertEquals("Recommended Exif Interoperability Rules (ExifR98)", descriptor.getDescription(TAG_INTEROP_INDEX));
         assertEquals("Recommended Exif Interoperability Rules (ExifR98)", descriptor.getInteropIndexDescription());
     }
 }
diff --git a/Tests/com/drew/metadata/exif/ExifReaderTest.java b/Tests/com/drew/metadata/exif/ExifReaderTest.java
index 2a129dd..1bf4ceb 100644
--- a/Tests/com/drew/metadata/exif/ExifReaderTest.java
+++ b/Tests/com/drew/metadata/exif/ExifReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
 package com.drew.metadata.exif;
 
 import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.ByteArrayReader;
 import com.drew.lang.Rational;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
@@ -29,6 +30,7 @@ import com.drew.tools.FileUtil;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.ArrayList;
 
 import static org.junit.Assert.*;
 
@@ -43,34 +45,25 @@ public class ExifReaderTest
     public static Metadata processBytes(@NotNull String filePath) throws IOException
     {
         Metadata metadata = new Metadata();
-        new ExifReader().extract(FileUtil.readBytes(filePath), metadata, JpegSegmentType.APP1);
+        byte[] bytes = FileUtil.readBytes(filePath);
+        new ExifReader().extract(new ByteArrayReader(bytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length(), null);
         return metadata;
     }
 
     @NotNull
     public static <T extends Directory> T processBytes(@NotNull String filePath, @NotNull Class<T> directoryClass) throws IOException
     {
-        T directory = processBytes(filePath).getDirectory(directoryClass);
+        T directory = processBytes(filePath).getFirstDirectoryOfType(directoryClass);
         assertNotNull(directory);
         return directory;
     }
 
+    @SuppressWarnings("ConstantConditions")
     @Test
     public void testExtractWithNullDataThrows() throws Exception
     {
         try{
-            new ExifReader().extract(null, new Metadata(), JpegSegmentType.APP1);
-            fail("Exception expected");
-        } catch (NullPointerException npe) {
-            // passed
-        }
-    }
-
-    @Test
-    public void testExtractWithNullMetadataThrows() throws Exception
-    {
-        try{
-            new ExifReader().extract(new byte[10], null, JpegSegmentType.APP1);
+            new ExifReader().readJpegSegments(null, new Metadata(), JpegSegmentType.APP1);
             fail("Exception expected");
         } catch (NullPointerException npe) {
             // passed
@@ -90,12 +83,15 @@ public class ExifReaderTest
     }
 
     @Test
-    public void testLoadJpegWithNoExifData() throws Exception
+    public void testReadJpegSegmentWithNoExifData() throws Exception
     {
         byte[] badExifData = new byte[]{ 1,2,3,4,5,6,7,8,9,10 };
         Metadata metadata = new Metadata();
-        new ExifReader().extract(badExifData, metadata, JpegSegmentType.APP1);
+        ArrayList<byte[]> segments = new ArrayList<byte[]>();
+        segments.add(badExifData);
+        new ExifReader().readJpegSegments(segments, metadata, JpegSegmentType.APP1);
         assertEquals(0, metadata.getDirectoryCount());
+        assertFalse(metadata.hasErrors());
     }
 
     @Test
@@ -156,21 +152,12 @@ public class ExifReaderTest
     }
 
     @Test
-    public void testThumbnailData() throws Exception
-    {
-        ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/manuallyAddedThumbnail.jpg.app1", ExifThumbnailDirectory.class);
-        byte[] thumbnailData = directory.getThumbnailData();
-        assertNotNull(thumbnailData);
-        assertEquals(2970, thumbnailData.length);
-    }
-
-    @Test
-    public void testThumbnailCompression() throws Exception
+    public void testCompression() throws Exception
     {
         ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/manuallyAddedThumbnail.jpg.app1", ExifThumbnailDirectory.class);
 
         // 6 means JPEG compression
-        assertEquals(6, directory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION));
+        assertEquals(6, directory.getInt(ExifThumbnailDirectory.TAG_COMPRESSION));
     }
 
     @Test
@@ -194,8 +181,8 @@ public class ExifReaderTest
         // These values used to be merged into a single directory, causing errors.
         // This unit test demonstrates correct behaviour.
         Metadata metadata = processBytes("Tests/Data/repeatedOrientationTagWithDifferentValues.jpg.app1");
-        ExifIFD0Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class);
-        ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
+        ExifIFD0Directory ifd0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+        ExifThumbnailDirectory thumbnailDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
 
         assertNotNull(ifd0Directory);
         assertNotNull(thumbnailDirectory);
diff --git a/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java b/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java
index fa9ed5f..d394eca 100644
--- a/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java
+++ b/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ package com.drew.metadata.exif;
 
 import org.junit.Test;
 
+import static com.drew.metadata.exif.ExifSubIFDDirectory.*;
 import static org.junit.Assert.assertEquals;
 
 /**
@@ -36,9 +37,9 @@ public class ExifSubIFDDescriptorTest
     {
         byte[] commentBytes = "\0\0\0\0\0\0\0\0This is a comment".getBytes();
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("This is a comment", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("This is a comment", descriptor.getDescription(TAG_USER_COMMENT));
     }
 
     @Test
@@ -46,9 +47,9 @@ public class ExifSubIFDDescriptorTest
     {
         byte[] commentBytes = "ASCII\0\0This is a comment".getBytes();
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("This is a comment", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("This is a comment", descriptor.getDescription(TAG_USER_COMMENT));
     }
 
     @Test
@@ -56,9 +57,9 @@ public class ExifSubIFDDescriptorTest
     {
         byte[] commentBytes = "ASCII\0\0\0          ".getBytes();
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("", descriptor.getDescription(TAG_USER_COMMENT));
     }
 
     @Test
@@ -67,9 +68,9 @@ public class ExifSubIFDDescriptorTest
         // the 10-byte encoding region is only partially full
         byte[] commentBytes = "ASCII\0\0\0".getBytes();
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("ASCII", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("ASCII", descriptor.getDescription(TAG_USER_COMMENT));
     }
 
     @Test
@@ -78,9 +79,9 @@ public class ExifSubIFDDescriptorTest
         // fill the 10-byte encoding region
         byte[] commentBytes = "ASCII\0\0\0\0\0".getBytes();
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("", descriptor.getDescription(TAG_USER_COMMENT));
     }
 
     @Test
@@ -88,9 +89,9 @@ public class ExifSubIFDDescriptorTest
     {
         byte[] commentBytes = new byte[] { 85, 78, 73, 67, 79, 68, 69, 0, 84, 0, 104, 0, 105, 0, 115, 0, 32, 0, 109, 0, 97, 0, 114, 0, 109, 0, 111, 0, 116, 0, 32, 0, 105, 0, 115, 0, 32, 0, 103, 0, 101, 0, 116, 0, 116, 0, 105, 0, 110, 0, 103, 0, 32, 0, 99, 0, 108, 0, 111, 0, 115, 0, 101, 0, 46, 0, 46, 0, 46, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 };
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("This marmot is getting close...", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("This marmot is getting close...", descriptor.getDescription(TAG_USER_COMMENT));
     }
 
     @Test
@@ -98,8 +99,8 @@ public class ExifSubIFDDescriptorTest
     {
         byte[] commentBytes = new byte[] { 65, 83, 67, 73, 73, 0, 0, 0, 73, 32, 97, 109, 32, 97, 32, 99, 111, 109, 109, 101, 110, 116, 46, 32, 89, 101, 121, 46, 0 };
         ExifSubIFDDirectory directory = new ExifSubIFDDirectory();
-        directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes);
+        directory.setByteArray(TAG_USER_COMMENT, commentBytes);
         ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory);
-        assertEquals("I am a comment. Yey.", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT));
+        assertEquals("I am a comment. Yey.", descriptor.getDescription(TAG_USER_COMMENT));
     }
 }
diff --git a/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java b/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java
index 8b1bf23..d3c46bd 100644
--- a/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java
+++ b/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ package com.drew.metadata.exif;
 
 import org.junit.Test;
 
+import static com.drew.metadata.exif.ExifThumbnailDirectory.*;
 import static org.junit.Assert.assertEquals;
 
 /**
@@ -36,15 +37,15 @@ public class ExifThumbnailDescriptorTest
     public void testGetYCbCrSubsamplingDescription() throws Exception
     {
         ExifThumbnailDirectory directory = new ExifThumbnailDirectory();
-        directory.setIntArray(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING, new int[]{2, 1});
+        directory.setIntArray(TAG_YCBCR_SUBSAMPLING, new int[]{2, 1});
 
         ExifThumbnailDescriptor descriptor = new ExifThumbnailDescriptor(directory);
-        assertEquals("YCbCr4:2:2", descriptor.getDescription(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING));
+        assertEquals("YCbCr4:2:2", descriptor.getDescription(TAG_YCBCR_SUBSAMPLING));
         assertEquals("YCbCr4:2:2", descriptor.getYCbCrSubsamplingDescription());
 
-        directory.setIntArray(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING, new int[]{2, 2});
+        directory.setIntArray(TAG_YCBCR_SUBSAMPLING, new int[]{2, 2});
 
-        assertEquals("YCbCr4:2:0", descriptor.getDescription(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING));
+        assertEquals("YCbCr4:2:0", descriptor.getDescription(TAG_YCBCR_SUBSAMPLING));
         assertEquals("YCbCr4:2:0", descriptor.getYCbCrSubsamplingDescription());
     }
 }
diff --git a/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java b/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java
index eb20bcd..94e99cd 100644
--- a/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java
+++ b/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -55,10 +55,10 @@ public class NikonType1MakernoteTest
     {
         Metadata metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType1.jpg.app1");
 
-        _nikonDirectory = metadata.getDirectory(NikonType1MakernoteDirectory.class);
-        _exifSubIFDDirectory = metadata.getDirectory(ExifSubIFDDirectory.class);
-        _exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class);
-        _thumbDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
+        _nikonDirectory = metadata.getFirstDirectoryOfType(NikonType1MakernoteDirectory.class);
+        _exifSubIFDDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
+        _exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+        _thumbDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
     }
 
     /*
@@ -173,7 +173,7 @@ public class NikonType1MakernoteTest
         assertEquals(3, _exifSubIFDDirectory.getInt(ExifSubIFDDirectory.TAG_FILE_SOURCE));
         assertEquals(1, _exifSubIFDDirectory.getInt(ExifSubIFDDirectory.TAG_SCENE_TYPE));
 
-        assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION));
+        assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_COMPRESSION));
         assertEquals(2036, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET));
         assertEquals(4662, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH));
     }
diff --git a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java
index caf7489..a8685ef 100644
--- a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java
+++ b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java
index 31f1915..a77c399 100644
--- a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java
+++ b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -29,11 +29,14 @@ import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
+import java.util.*;
+
 /**
  * @author Drew Noakes https://drewnoakes.com
  */
 public class NikonType2MakernoteTest2
 {
+    private Metadata _metadata;
     private NikonType2MakernoteDirectory _nikonDirectory;
     private ExifIFD0Directory _exifIFD0Directory;
     private ExifSubIFDDirectory _exifSubIFDDirectory;
@@ -42,12 +45,12 @@ public class NikonType2MakernoteTest2
     @Before
     public void setUp() throws Exception
     {
-        Metadata metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType2b.jpg.app1");
+        _metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType2b.jpg.app1");
 
-        _nikonDirectory = metadata.getDirectory(NikonType2MakernoteDirectory.class);
-        _exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class);
-        _exifSubIFDDirectory = metadata.getDirectory(ExifSubIFDDirectory.class);
-        _thumbDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
+        _nikonDirectory = _metadata.getFirstDirectoryOfType(NikonType2MakernoteDirectory.class);
+        _exifIFD0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
+        _exifSubIFDDirectory = _metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
+        _thumbDirectory = _metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
 
         assertNotNull(_nikonDirectory);
         assertNotNull(_exifSubIFDDirectory);
@@ -97,7 +100,35 @@ public class NikonType2MakernoteTest2
         assertEquals("                ", _nikonDirectory.getString(0x008f));
         assertEquals(0, _nikonDirectory.getInt(0x0094));
         assertEquals("FPNR", _nikonDirectory.getString(0x0095));
-        assertEquals("80 114 105 110 116 73 77 0 48 49 48 48 0 0 13 0 1 0 22 0 22 0 2 0 1 0 0 0 3 0 94 0 0 0 7 0 0 0 0 0 8 0 0 0 0 0 9 0 0 0 0 0 10 0 0 0 0 0 11 0 -90 0 0 0 12 0 0 0 0 0 13 0 0 0 0 0 14 0 -66 0 0 0 0 1 5 0 0 0 1 1 1 0 0 0 9 17 0 0 16 39 0 0 11 15 0 0 16 39 0 0 -105 5 0 0 16 39 0 0 -80 8 0 0 16 39 0 0 1 28 0 0 16 39 0 0 94 2 0 0 16 39 0 0 -117 0 0 0 16 39 0 0 -53 3 0 0 16 39 0 0 -27 27 0 0 16 39 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", _nikonDirectory.getString(0x0e00));
+
+        // PrintIM
+        HashMap<Integer, String> _expectedData = new HashMap<Integer, String>();
+        _expectedData.put(0x0000, "0100");
+        _expectedData.put(0x0001, "0x00160016");
+        _expectedData.put(0x0002, "0x00000001");
+        _expectedData.put(0x0003, "0x0000005e");
+        _expectedData.put(0x0007, "0x00000000");
+        _expectedData.put(0x0008, "0x00000000");
+        _expectedData.put(0x0009, "0x00000000");
+        _expectedData.put(0x000A, "0x00000000");
+        _expectedData.put(0x000B, "0x000000a6");
+        _expectedData.put(0x000C, "0x00000000");
+        _expectedData.put(0x000D, "0x00000000");
+        _expectedData.put(0x000E, "0x000000be");
+        _expectedData.put(0x0100, "0x00000005");
+        _expectedData.put(0x0101, "0x00000001");
+
+        PrintIMDirectory nikonPrintImDirectory = _metadata.getFirstDirectoryOfType(PrintIMDirectory.class);
+
+        assertNotNull(nikonPrintImDirectory);
+
+        assertEquals(_expectedData.size(), nikonPrintImDirectory.getTagCount());
+        for (Map.Entry<Integer, String> _expected : _expectedData.entrySet())
+        {
+            assertEquals(_expected.getValue(), nikonPrintImDirectory.getDescription(_expected.getKey()));
+        }
+
+//        assertEquals("80 114 105 110 116 73 77 0 48 49 48 48 0 0 13 0 1 0 22 0 22 0 2 0 1 0 0 0 3 0 94 0 0 0 7 0 0 0 0 0 8 0 0 0 0 0 9 0 0 0 0 0 10 0 0 0 0 0 11 0 166 0 0 0 12 0 0 0 0 0 13 0 0 0 0 0 14 0 190 0 0 0 0 1 5 0 0 0 1 1 1 0 0 0 9 17 0 0 16 39 0 0 11 15 0 0 16 39 0 0 151 5 0 0 16 39 0 0 176 8 0 0 16 39 0 0 1 28 0 0 16 39 0 0 94 2 0 0 16 39 0 0 139 0 0 0 16 39 0 0 203 3 0 0 16 39 0 0 229 27 0 0 16 39 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", _nikonDirectory.getString(0x0e00));
 //        assertEquals("PrintIM", _nikonDirectory.getString(0x0e00));
         assertEquals(1394, _nikonDirectory.getInt(0x0e10));
     }
@@ -181,7 +212,7 @@ public class NikonType2MakernoteTest2
     @Test
     public void testExifThumbnailDirectory_MatchesKnownValues() throws Exception
     {
-        assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION));
+        assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_COMPRESSION));
         assertEquals(1494, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET));
         assertEquals(6077, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH));
         assertEquals(1494, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET));
diff --git a/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java b/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java
index ff328bc..e71f941 100644
--- a/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java
+++ b/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java b/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java
index abff3f8..38466a9 100644
--- a/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java
+++ b/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java b/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java
index b28fa0e..eca2ee1 100644
--- a/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java
+++ b/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/gif/GifReaderTest.java b/Tests/com/drew/metadata/gif/GifReaderTest.java
index a45b209..08fbd48 100644
--- a/Tests/com/drew/metadata/gif/GifReaderTest.java
+++ b/Tests/com/drew/metadata/gif/GifReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -44,7 +44,7 @@ public class GifReaderTest
         new GifReader().extract(new StreamReader(stream), metadata);
         stream.close();
 
-        GifHeaderDirectory directory = metadata.getDirectory(GifHeaderDirectory.class);
+        GifHeaderDirectory directory = metadata.getFirstDirectoryOfType(GifHeaderDirectory.class);
         assertNotNull(directory);
         return directory;
     }
@@ -63,7 +63,7 @@ public class GifReaderTest
         assertFalse(directory.getBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED));
         assertEquals(8, directory.getInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL));
         assertTrue(directory.getBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE));
-        assertEquals(0, directory.getInt(GifHeaderDirectory.TAG_TRANSPARENT_COLOR_INDEX));
+        assertEquals(0, directory.getInt(GifHeaderDirectory.TAG_BACKGROUND_COLOR_INDEX));
     }
 
     @Test
@@ -80,6 +80,6 @@ public class GifReaderTest
         assertFalse(directory.getBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED));
         assertEquals(5, directory.getInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL));
         assertTrue(directory.getBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE));
-        assertEquals(8, directory.getInt(GifHeaderDirectory.TAG_TRANSPARENT_COLOR_INDEX));
+        assertEquals(8, directory.getInt(GifHeaderDirectory.TAG_BACKGROUND_COLOR_INDEX));
     }
 }
diff --git a/Tests/com/drew/metadata/icc/IccReaderTest.java b/Tests/com/drew/metadata/icc/IccReaderTest.java
index 447d145..04dd6bc 100644
--- a/Tests/com/drew/metadata/icc/IccReaderTest.java
+++ b/Tests/com/drew/metadata/icc/IccReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,35 +21,67 @@
 
 package com.drew.metadata.icc;
 
+import com.drew.imaging.jpeg.JpegSegmentType;
 import com.drew.lang.ByteArrayReader;
 import com.drew.metadata.Metadata;
 import com.drew.testing.TestHelper;
 import com.drew.tools.FileUtil;
 import org.junit.Test;
 
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
+@SuppressWarnings("ConstantConditions")
 public class IccReaderTest
 {
+    // TODO add a test with well-formed ICC data and assert output values are correct
+
     @Test
-    public void testExtract() throws Exception
+    public void testExtract_InvalidData() throws Exception
     {
         byte[] app2Bytes = FileUtil.readBytes("Tests/Data/iccDataInvalid1.jpg.app2");
 
-        // ICC data starts after a 14-byte preamble
+        // When in an APP2 segment, ICC data starts after a 14-byte preamble
         byte[] icc = TestHelper.skipBytes(app2Bytes, 14);
 
         Metadata metadata = new Metadata();
         new IccReader().extract(new ByteArrayReader(icc), metadata);
 
-        IccDirectory directory = metadata.getDirectory(IccDirectory.class);
+        IccDirectory directory = metadata.getFirstDirectoryOfType(IccDirectory.class);
 
         assertNotNull(directory);
+        assertTrue(directory.hasErrors());
+    }
+
+    @Test
+    public void testReadJpegSegments_InvalidData() throws Exception
+    {
+        byte[] app2Bytes = FileUtil.readBytes("Tests/Data/iccDataInvalid1.jpg.app2");
 
-        // TODO validate expected values
+        Metadata metadata = new Metadata();
+        new IccReader().readJpegSegments(Arrays.asList(app2Bytes), metadata, JpegSegmentType.APP2);
 
-//        for (Tag tag : directory.getTags()) {
-//            System.out.println(tag);
-//        }
+        IccDirectory directory = metadata.getFirstDirectoryOfType(IccDirectory.class);
+
+        assertNotNull(directory);
+        assertTrue(directory.hasErrors());
+    }
+
+    @Test
+    public void testExtract_ProfileDateTime() throws Exception
+    {
+        byte[] app2Bytes = FileUtil.readBytes("Tests/Data/withExifAndIptc.jpg.app2");
+
+        Metadata metadata = new Metadata();
+        new IccReader().readJpegSegments(Arrays.asList(app2Bytes), metadata, JpegSegmentType.APP2);
+
+        IccDirectory directory = metadata.getFirstDirectoryOfType(IccDirectory.class);
+
+        assertNotNull(directory);
+        assertEquals("1998:02:09 06:49:00", directory.getString(IccDirectory.TAG_PROFILE_DATETIME));
+        assertEquals(887006940000L, directory.getDate(IccDirectory.TAG_PROFILE_DATETIME).getTime());
     }
 }
diff --git a/Tests/com/drew/metadata/iptc/IptcDirectoryTest.java b/Tests/com/drew/metadata/iptc/IptcDirectoryTest.java
new file mode 100644
index 0000000..95efa9f
--- /dev/null
+++ b/Tests/com/drew/metadata/iptc/IptcDirectoryTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.iptc;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import static org.junit.Assert.assertEquals;
+
+@SuppressWarnings("ConstantConditions")
+public class IptcDirectoryTest
+{
+    private IptcDirectory _directory;
+
+    @Before
+    public void setUp()
+    {
+        _directory = new IptcDirectory();
+    }
+
+    @Test
+    public void testGetDateSent()
+    {
+        _directory.setString(IptcDirectory.TAG_DATE_SENT, "20101212");
+        _directory.setString(IptcDirectory.TAG_TIME_SENT, "124135+0100");
+        final Date actual = _directory.getDateSent();
+
+        Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35);
+        calendar.setTimeZone(TimeZone.getTimeZone("GMT+1"));
+        assertEquals(calendar.getTime(), actual);
+        assertEquals(1292154095000L, actual.getTime());
+    }
+
+    @Test
+    public void testGetReleaseDate()
+    {
+        _directory.setString(IptcDirectory.TAG_RELEASE_DATE, "20101212");
+        _directory.setString(IptcDirectory.TAG_RELEASE_TIME, "124135+0100");
+        final Date actual = _directory.getReleaseDate();
+
+        Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35);
+        calendar.setTimeZone(TimeZone.getTimeZone("GMT+1"));
+        assertEquals(calendar.getTime(), actual);
+        assertEquals(1292154095000L, actual.getTime());
+    }
+
+    @Test
+    public void testGetExpirationDate()
+    {
+        _directory.setString(IptcDirectory.TAG_EXPIRATION_DATE, "20101212");
+        _directory.setString(IptcDirectory.TAG_EXPIRATION_TIME, "124135+0100");
+        final Date actual = _directory.getExpirationDate();
+
+        Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35);
+        calendar.setTimeZone(TimeZone.getTimeZone("GMT+1"));
+        assertEquals(calendar.getTime(), actual);
+        assertEquals(1292154095000L, actual.getTime());
+    }
+
+    @Test
+    public void testGetDateCreated()
+    {
+        _directory.setString(IptcDirectory.TAG_DATE_CREATED, "20101212");
+        _directory.setString(IptcDirectory.TAG_TIME_CREATED, "124135+0100");
+        final Date actual = _directory.getDateCreated();
+
+        Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35);
+        calendar.setTimeZone(TimeZone.getTimeZone("GMT+1"));
+        assertEquals(calendar.getTime(), actual);
+        assertEquals(1292154095000L, actual.getTime());
+    }
+
+    @Test
+    public void testGetDigitalDateCreated()
+    {
+        _directory.setString(IptcDirectory.TAG_DIGITAL_DATE_CREATED, "20101212");
+        _directory.setString(IptcDirectory.TAG_DIGITAL_TIME_CREATED, "124135+0100");
+        final Date actual = _directory.getDigitalDateCreated();
+
+        Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35);
+        calendar.setTimeZone(TimeZone.getTimeZone("GMT+1"));
+        assertEquals(calendar.getTime(), actual);
+        assertEquals(1292154095000L, actual.getTime());
+    }
+}
diff --git a/Tests/com/drew/metadata/iptc/IptcReaderTest.java b/Tests/com/drew/metadata/iptc/IptcReaderTest.java
index 8c37656..23e0581 100644
--- a/Tests/com/drew/metadata/iptc/IptcReaderTest.java
+++ b/Tests/com/drew/metadata/iptc/IptcReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import static org.junit.Assert.*;
  *
  * @author Drew Noakes https://drewnoakes.com
  */
+@SuppressWarnings("ConstantConditions")
 public class IptcReaderTest
 {
     @NotNull
@@ -44,7 +45,7 @@ public class IptcReaderTest
         Metadata metadata = new Metadata();
         byte[] bytes = FileUtil.readBytes(filePath);
         new IptcReader().extract(new SequentialByteArrayReader(bytes), metadata, bytes.length);
-        IptcDirectory directory = metadata.getDirectory(IptcDirectory.class);
+        IptcDirectory directory = metadata.getFirstDirectoryOfType(IptcDirectory.class);
         assertNotNull(directory);
         return directory;
     }
@@ -63,49 +64,49 @@ public class IptcReaderTest
         assertArrayEquals(new String[] { "Supl. Category2", "Supl. Category1", "Cat" }, directory.getStringArray(tags[0].getTagType()));
 
         assertEquals(IptcDirectory.TAG_COPYRIGHT_NOTICE, tags[1].getTagType());
-        assertEquals("Copyright", directory.getObject(tags[1].getTagType()));
+        assertEquals("Copyright", directory.getString(tags[1].getTagType()));
 
         assertEquals(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS, tags[2].getTagType());
-        assertEquals("Special Instr.", directory.getObject(tags[2].getTagType()));
+        assertEquals("Special Instr.", directory.getString(tags[2].getTagType()));
 
         assertEquals(IptcDirectory.TAG_HEADLINE, tags[3].getTagType());
-        assertEquals("Headline", directory.getObject(tags[3].getTagType()));
+        assertEquals("Headline", directory.getString(tags[3].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION_WRITER, tags[4].getTagType());
-        assertEquals("CaptionWriter", directory.getObject(tags[4].getTagType()));
+        assertEquals("CaptionWriter", directory.getString(tags[4].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION, tags[5].getTagType());
-        assertEquals("Caption", directory.getObject(tags[5].getTagType()));
+        assertEquals("Caption", directory.getString(tags[5].getTagType()));
 
         assertEquals(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE, tags[6].getTagType());
-        assertEquals("Transmission", directory.getObject(tags[6].getTagType()));
+        assertEquals("Transmission", directory.getString(tags[6].getTagType()));
 
         assertEquals(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME, tags[7].getTagType());
-        assertEquals("Country", directory.getObject(tags[7].getTagType()));
+        assertEquals("Country", directory.getString(tags[7].getTagType()));
 
         assertEquals(IptcDirectory.TAG_PROVINCE_OR_STATE, tags[8].getTagType());
-        assertEquals("State", directory.getObject(tags[8].getTagType()));
+        assertEquals("State", directory.getString(tags[8].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CITY, tags[9].getTagType());
-        assertEquals("City", directory.getObject(tags[9].getTagType()));
+        assertEquals("City", directory.getString(tags[9].getTagType()));
 
         assertEquals(IptcDirectory.TAG_DATE_CREATED, tags[10].getTagType());
-        assertEquals(new java.util.GregorianCalendar(2000, 0, 1).getTime(), directory.getObject(tags[10].getTagType()));
+        assertEquals("20000101", directory.getString(tags[10].getTagType()));
 
         assertEquals(IptcDirectory.TAG_OBJECT_NAME, tags[11].getTagType());
-        assertEquals("ObjectName", directory.getObject(tags[11].getTagType()));
+        assertEquals("ObjectName", directory.getString(tags[11].getTagType()));
 
         assertEquals(IptcDirectory.TAG_SOURCE, tags[12].getTagType());
-        assertEquals("Source", directory.getObject(tags[12].getTagType()));
+        assertEquals("Source", directory.getString(tags[12].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CREDIT, tags[13].getTagType());
-        assertEquals("Credits", directory.getObject(tags[13].getTagType()));
+        assertEquals("Credits", directory.getString(tags[13].getTagType()));
 
         assertEquals(IptcDirectory.TAG_BY_LINE_TITLE, tags[14].getTagType());
-        assertEquals("BylineTitle", directory.getObject(tags[14].getTagType()));
+        assertEquals("BylineTitle", directory.getString(tags[14].getTagType()));
 
         assertEquals(IptcDirectory.TAG_BY_LINE, tags[15].getTagType());
-        assertEquals("Byline", directory.getObject(tags[15].getTagType()));
+        assertEquals("Byline", directory.getString(tags[15].getTagType()));
     }
 
     @Test
@@ -122,52 +123,52 @@ public class IptcReaderTest
         assertEquals(2, directory.getObject(tags[0].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION, tags[1].getTagType());
-        assertEquals("Caption PS6", directory.getObject(tags[1].getTagType()));
+        assertEquals("Caption PS6", directory.getString(tags[1].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION_WRITER, tags[2].getTagType());
-        assertEquals("CaptionWriter", directory.getObject(tags[2].getTagType()));
+        assertEquals("CaptionWriter", directory.getString(tags[2].getTagType()));
 
         assertEquals(IptcDirectory.TAG_HEADLINE, tags[3].getTagType());
-        assertEquals("Headline", directory.getObject(tags[3].getTagType()));
+        assertEquals("Headline", directory.getString(tags[3].getTagType()));
 
         assertEquals(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS, tags[4].getTagType());
-        assertEquals("Special Instr.", directory.getObject(tags[4].getTagType()));
+        assertEquals("Special Instr.", directory.getString(tags[4].getTagType()));
 
         assertEquals(IptcDirectory.TAG_BY_LINE, tags[5].getTagType());
-        assertEquals("Byline", directory.getObject(tags[5].getTagType()));
+        assertEquals("Byline", directory.getString(tags[5].getTagType()));
 
         assertEquals(IptcDirectory.TAG_BY_LINE_TITLE, tags[6].getTagType());
-        assertEquals("BylineTitle", directory.getObject(tags[6].getTagType()));
+        assertEquals("BylineTitle", directory.getString(tags[6].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CREDIT, tags[7].getTagType());
-        assertEquals("Credits", directory.getObject(tags[7].getTagType()));
+        assertEquals("Credits", directory.getString(tags[7].getTagType()));
 
         assertEquals(IptcDirectory.TAG_SOURCE, tags[8].getTagType());
-        assertEquals("Source", directory.getObject(tags[8].getTagType()));
+        assertEquals("Source", directory.getString(tags[8].getTagType()));
 
         assertEquals(IptcDirectory.TAG_OBJECT_NAME, tags[9].getTagType());
-        assertEquals("ObjectName", directory.getObject(tags[9].getTagType()));
+        assertEquals("ObjectName", directory.getString(tags[9].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CITY, tags[10].getTagType());
-        assertEquals("City", directory.getObject(tags[10].getTagType()));
+        assertEquals("City", directory.getString(tags[10].getTagType()));
 
         assertEquals(IptcDirectory.TAG_PROVINCE_OR_STATE, tags[11].getTagType());
-        assertEquals("State", directory.getObject(tags[11].getTagType()));
+        assertEquals("State", directory.getString(tags[11].getTagType()));
 
         assertEquals(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME, tags[12].getTagType());
-        assertEquals("Country", directory.getObject(tags[12].getTagType()));
+        assertEquals("Country", directory.getString(tags[12].getTagType()));
 
         assertEquals(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE, tags[13].getTagType());
-        assertEquals("Transmission", directory.getObject(tags[13].getTagType()));
+        assertEquals("Transmission", directory.getString(tags[13].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CATEGORY, tags[14].getTagType());
-        assertEquals("Cat", directory.getObject(tags[14].getTagType()));
+        assertEquals("Cat", directory.getString(tags[14].getTagType()));
 
         assertEquals(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES, tags[15].getTagType());
         assertArrayEquals(new String[] { "Supl. Category1", "Supl. Category2" }, directory.getStringArray(tags[15].getTagType()));
 
         assertEquals(IptcDirectory.TAG_COPYRIGHT_NOTICE, tags[16].getTagType());
-        assertEquals("Copyright", directory.getObject(tags[16].getTagType()));
+        assertEquals("Copyright", directory.getString(tags[16].getTagType()));
     }
 
     @Test
@@ -190,7 +191,7 @@ public class IptcReaderTest
         assertEquals(2, directory.getObject(tags[2].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION, tags[3].getTagType());
-        assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getObject(tags[3].getTagType()));
+        assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getStringValue(tags[3].getTagType()).toString());
     }
 
     @Test
@@ -210,7 +211,7 @@ public class IptcReaderTest
         assertEquals(2, directory.getObject(tags[1].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION, tags[2].getTagType());
-        assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getObject(tags[2].getTagType()));
+        assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getStringValue(tags[2].getTagType()).toString());
     }
 
     @Test
@@ -227,7 +228,7 @@ public class IptcReaderTest
         assertEquals(2, directory.getObject(tags[0].getTagType()));
 
         assertEquals(IptcDirectory.TAG_CAPTION, tags[1].getTagType());
-        assertEquals("Das Encoding dieser Metadaten ist nicht deklariert und lässt sich nur schwer erkennen.", directory.getObject(tags[1].getTagType()));
+        assertEquals("Das Encoding dieser Metadaten ist nicht deklariert und lässt sich nur schwer erkennen.", directory.getStringValue(tags[1].getTagType()).toString());
 
         assertEquals(IptcDirectory.TAG_KEYWORDS, tags[2].getTagType());
         assertArrayEquals(new String[]{"häufig", "üblich", "Lösung", "Spaß"}, directory.getStringArray(tags[2].getTagType()));
diff --git a/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java b/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java
index 3365898..25dbc8a 100644
--- a/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java
+++ b/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java
@@ -1,9 +1,29 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
 package com.drew.metadata.iptc;
 
-import static org.junit.Assert.assertEquals;
-
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
+
 public class Iso2022ConverterTest
 {
     @Test
diff --git a/Tests/com/drew/metadata/jfif/JfifReaderTest.java b/Tests/com/drew/metadata/jfif/JfifReaderTest.java
index 6bd84a2..3fb4b49 100644
--- a/Tests/com/drew/metadata/jfif/JfifReaderTest.java
+++ b/Tests/com/drew/metadata/jfif/JfifReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -49,12 +49,12 @@ public class JfifReaderTest
         reader.extract(new ByteArrayReader(jfifData), metadata);
 
         assertEquals(1, metadata.getDirectoryCount());
-        JfifDirectory directory = metadata.getDirectory(JfifDirectory.class);
+        JfifDirectory directory = metadata.getFirstDirectoryOfType(JfifDirectory.class);
         assertNotNull(directory);
         assertFalse(directory.getErrors().toString(), directory.hasErrors());
 
         Tag[] tags = directory.getTags().toArray(new Tag[directory.getTagCount()]);
-        assertEquals(4, tags.length);
+        assertEquals(6, tags.length);
 
         assertEquals(JfifDirectory.TAG_VERSION, tags[0].getTagType());
         assertEquals(0x0102, directory.getInt(tags[0].getTagType()));
@@ -67,5 +67,11 @@ public class JfifReaderTest
 
         assertEquals(JfifDirectory.TAG_RESY, tags[3].getTagType());
         assertEquals(108, directory.getInt(tags[3].getTagType()));
+
+        assertEquals(JfifDirectory.TAG_THUMB_WIDTH, tags[4].getTagType());
+        assertEquals(0, directory.getInt(tags[4].getTagType()));
+
+        assertEquals(JfifDirectory.TAG_THUMB_HEIGHT, tags[5].getTagType());
+        assertEquals(0, directory.getInt(tags[5].getTagType()));
     }
 }
diff --git a/Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java b/Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java
new file mode 100644
index 0000000..567d438
--- /dev/null
+++ b/Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.drew.metadata.jpeg.HuffmanTablesDirectory.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author Nadahar
+ */
+public class HuffmanTablesDescriptorTest
+{
+    private HuffmanTablesDirectory _directory;
+    private HuffmanTablesDescriptor _descriptor;
+
+    @Before
+    public void setUp() throws Exception
+    {
+        _directory = new HuffmanTablesDirectory();
+        _descriptor = new HuffmanTablesDescriptor(_directory);
+    }
+
+    @Test
+    public void testGetNumberOfTablesDescription() throws Exception
+    {
+        assertNull(_descriptor.getNumberOfTablesDescription());
+        _directory.setInt(TAG_NUMBER_OF_TABLES, 0);
+        assertEquals("0 Huffman tables", _descriptor.getNumberOfTablesDescription());
+        assertEquals("0 Huffman tables", _descriptor.getDescription(TAG_NUMBER_OF_TABLES));
+        _directory.setInt(TAG_NUMBER_OF_TABLES, 1);
+        assertEquals("1 Huffman table", _descriptor.getNumberOfTablesDescription());
+        assertEquals("1 Huffman table", _descriptor.getDescription(TAG_NUMBER_OF_TABLES));
+        _directory.setInt(TAG_NUMBER_OF_TABLES, 3);
+        assertEquals("3 Huffman tables", _descriptor.getNumberOfTablesDescription());
+        assertEquals("3 Huffman tables", _descriptor.getDescription(TAG_NUMBER_OF_TABLES));
+
+    }
+}
diff --git a/Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java b/Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java
new file mode 100644
index 0000000..3f77faf
--- /dev/null
+++ b/Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass;
+
+/**
+ * @author Nadahar
+ */
+public class HuffmanTablesDirectoryTest
+{
+    private HuffmanTablesDirectory _directory;
+
+    @Before
+    public void setUp()
+    {
+        _directory = new HuffmanTablesDirectory();
+    }
+
+    @Test
+    public void testSetAndGetValue() throws Exception
+    {
+        _directory.setInt(32, 8);
+        assertEquals(8, _directory.getInt(32));
+    }
+
+    @Test
+    public void testGetComponent_NotAdded()
+    {
+        try {
+            _directory.getTable(1);
+            fail();
+        } catch (IndexOutOfBoundsException e) {
+            // Expected exception
+        }
+    }
+
+    @Test
+    public void testGetNumberOfTables() throws Exception
+    {
+        _directory.setInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES, 9);
+        assertEquals(9,_directory.getNumberOfTables());
+        assertEquals("9 Huffman tables", _directory.getDescription(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES));
+    }
+
+    @Test
+    public void testIsTypical() throws Exception
+    {
+        _directory.tables.add(new HuffmanTable(
+            HuffmanTableClass.AC,
+            0,
+            HuffmanTablesDirectory.TYPICAL_CHROMINANCE_AC_LENGTHS,
+            HuffmanTablesDirectory.TYPICAL_CHROMINANCE_AC_VALUES
+        ));
+        _directory.tables.add(new HuffmanTable(
+            HuffmanTableClass.DC,
+            0,
+            HuffmanTablesDirectory.TYPICAL_LUMINANCE_DC_LENGTHS,
+            HuffmanTablesDirectory.TYPICAL_LUMINANCE_DC_VALUES
+        ));
+
+        assertTrue(_directory.getTable(0).isTypical());
+        assertFalse(_directory.getTable(0).isOptimized());
+        assertTrue(_directory.getTable(1).isTypical());
+        assertFalse(_directory.getTable(1).isOptimized());
+
+        assertTrue(_directory.isTypical());
+        assertFalse(_directory.isOptimized());
+    }
+}
diff --git a/Tests/com/drew/metadata/jpeg/JpegComponentTest.java b/Tests/com/drew/metadata/jpeg/JpegComponentTest.java
index 3ae1fa8..12a7466 100644
--- a/Tests/com/drew/metadata/jpeg/JpegComponentTest.java
+++ b/Tests/com/drew/metadata/jpeg/JpegComponentTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java b/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java
index a804df5..efdaf84 100644
--- a/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java
+++ b/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import com.drew.metadata.MetadataException;
 import org.junit.Before;
 import org.junit.Test;
 
+import static com.drew.metadata.jpeg.JpegDirectory.*;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
@@ -51,33 +52,33 @@ public class JpegDescriptorTest
     @Test
     public void testGetImageWidthDescription() throws Exception
     {
-        _directory.setInt(JpegDirectory.TAG_IMAGE_WIDTH, 123);
+        _directory.setInt(TAG_IMAGE_WIDTH, 123);
         assertEquals("123 pixels", _descriptor.getImageWidthDescription());
-        assertEquals("123 pixels", _directory.getDescription(JpegDirectory.TAG_IMAGE_WIDTH));
+        assertEquals("123 pixels", _directory.getDescription(TAG_IMAGE_WIDTH));
     }
 
     @Test
     public void testGetImageHeightDescription() throws Exception
     {
-        _directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, 123);
+        _directory.setInt(TAG_IMAGE_HEIGHT, 123);
         assertEquals("123 pixels", _descriptor.getImageHeightDescription());
-        assertEquals("123 pixels", _directory.getDescription(JpegDirectory.TAG_IMAGE_HEIGHT));
+        assertEquals("123 pixels", _directory.getDescription(TAG_IMAGE_HEIGHT));
     }
 
     @Test
     public void testGetDataPrecisionDescription() throws Exception
     {
-        _directory.setInt(JpegDirectory.TAG_DATA_PRECISION, 8);
+        _directory.setInt(TAG_DATA_PRECISION, 8);
         assertEquals("8 bits", _descriptor.getDataPrecisionDescription());
-        assertEquals("8 bits", _directory.getDescription(JpegDirectory.TAG_DATA_PRECISION));
+        assertEquals("8 bits", _directory.getDescription(TAG_DATA_PRECISION));
     }
 
     @Test
     public void testGetComponentDescription() throws MetadataException
     {
         JpegComponent component1 = new JpegComponent(1, 0x22, 0);
-        _directory.setObject(JpegDirectory.TAG_COMPONENT_DATA_1, component1);
-        assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", _directory.getDescription(JpegDirectory.TAG_COMPONENT_DATA_1));
+        _directory.setObject(TAG_COMPONENT_DATA_1, component1);
+        assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", _directory.getDescription(TAG_COMPONENT_DATA_1));
         assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", _descriptor.getComponentDataDescription(0));
     }
 }
diff --git a/Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java b/Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java
new file mode 100644
index 0000000..ba40def
--- /dev/null
+++ b/Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.jpeg;
+
+import com.drew.imaging.jpeg.JpegSegmentData;
+import com.drew.imaging.jpeg.JpegSegmentReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass;
+import org.junit.Before;
+import org.junit.Test;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Nadahar
+ */
+public class JpegDhtReaderTest
+{
+    @NotNull
+    public static HuffmanTablesDirectory processBytes(String filePath) throws Exception
+    {
+        Metadata metadata = new Metadata();
+        JpegSegmentData segmentData = JpegSegmentReader.readSegments(
+            new File(filePath),
+            Collections.singletonList(JpegSegmentType.DHT));
+
+        Iterable<byte[]> segments = segmentData.getSegments(JpegSegmentType.DHT);
+        for (byte[] segment : segments) {
+            new JpegDhtReader().extract(new SequentialByteArrayReader(segment), metadata);
+        }
+
+
+        HuffmanTablesDirectory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class);
+        assertNotNull(directory);
+        assertEquals(1, metadata.getDirectoriesOfType(HuffmanTablesDirectory.class).size());
+        return directory;
+    }
+
+    private HuffmanTablesDirectory _directory;
+
+    @Before
+    public void setUp() throws Exception
+    {
+        _directory = processBytes("Tests/Data/withExifAndIptc.jpg");
+    }
+
+    @Test
+    public void testExtract_NumberOfTables() throws Exception
+    {
+        assertEquals(4, _directory.getInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES));
+        assertEquals(4, _directory.getNumberOfTables());
+    }
+
+    @Test
+    public void testExtract_Tables() throws Exception
+    {
+        byte[] l = {0, 1, 4, 1, 2, 3, 3, 8, 5, 9, 6, 4, 6, 2, 3, 0};
+        byte[] v = {0, 1, 3, 2, 4, 5};
+
+        assertArrayEquals(l, _directory.getTable(1).getLengthBytes());
+        assertArrayEquals(v, _directory.getTable(2).getValueBytes());
+        assertEquals(HuffmanTableClass.DC, _directory.getTable(0).getTableClass());
+        assertEquals(HuffmanTableClass.AC, _directory.getTable(1).getTableClass());
+        assertEquals(HuffmanTableClass.DC, _directory.getTable(2).getTableClass());
+        assertEquals(HuffmanTableClass.AC, _directory.getTable(3).getTableClass());
+        assertEquals(0, _directory.getTable(0).getTableDestinationId());
+        assertEquals(0, _directory.getTable(1).getTableDestinationId());
+        assertEquals(1, _directory.getTable(2).getTableDestinationId());
+        assertEquals(1, _directory.getTable(3).getTableDestinationId());
+        assertEquals(25, _directory.getTable(0).getTableLength());
+        assertEquals(74, _directory.getTable(1).getTableLength());
+        assertEquals(23, _directory.getTable(2).getTableLength());
+        assertEquals(38, _directory.getTable(3).getTableLength());
+    }
+}
diff --git a/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java b/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java
index e6b65b3..5e2a565 100644
--- a/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java
+++ b/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/Tests/com/drew/metadata/jpeg/JpegReaderTest.java b/Tests/com/drew/metadata/jpeg/JpegReaderTest.java
index b433938..766de5f 100644
--- a/Tests/com/drew/metadata/jpeg/JpegReaderTest.java
+++ b/Tests/com/drew/metadata/jpeg/JpegReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -44,7 +44,7 @@ public class JpegReaderTest
         Metadata metadata = new Metadata();
         new JpegReader().extract(FileUtil.readBytes(filePath), metadata, JpegSegmentType.SOF0);
 
-        JpegDirectory directory = metadata.getDirectory(JpegDirectory.class);
+        JpegDirectory directory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
         assertNotNull(directory);
         return directory;
     }
diff --git a/Tests/com/drew/metadata/photoshop/PsdReaderTest.java b/Tests/com/drew/metadata/photoshop/PsdReaderTest.java
index fdbb4b5..8d77df0 100644
--- a/Tests/com/drew/metadata/photoshop/PsdReaderTest.java
+++ b/Tests/com/drew/metadata/photoshop/PsdReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,13 +21,14 @@
 
 package com.drew.metadata.photoshop;
 
-import com.drew.lang.RandomAccessFileReader;
+import com.drew.lang.StreamReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
 import org.junit.Test;
 
 import java.io.File;
-import java.io.RandomAccessFile;
+import java.io.FileInputStream;
+import java.io.InputStream;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -41,11 +42,15 @@ public class PsdReaderTest
     public static PsdHeaderDirectory processBytes(@NotNull String file) throws Exception
     {
         Metadata metadata = new Metadata();
-        RandomAccessFile randomAccessFile = new RandomAccessFile(new File(file), "r");
-        new PsdReader().extract(new RandomAccessFileReader(randomAccessFile), metadata);
-        randomAccessFile.close();
+        InputStream stream = new FileInputStream(new File(file));
+        try {
+            new PsdReader().extract(new StreamReader(stream), metadata);
+        } catch (Exception e) {
+            stream.close();
+            throw e;
+        }
 
-        PsdHeaderDirectory directory = metadata.getDirectory(PsdHeaderDirectory.class);
+        PsdHeaderDirectory directory = metadata.getFirstDirectoryOfType(PsdHeaderDirectory.class);
         assertNotNull(directory);
         return directory;
     }
diff --git a/Tests/com/drew/metadata/xmp/XmpReaderTest.java b/Tests/com/drew/metadata/xmp/XmpReaderTest.java
index 492da51..e58792a 100644
--- a/Tests/com/drew/metadata/xmp/XmpReaderTest.java
+++ b/Tests/com/drew/metadata/xmp/XmpReaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
@@ -21,14 +21,11 @@
 package com.drew.metadata.xmp;
 
 import com.drew.imaging.jpeg.JpegSegmentType;
-import com.drew.lang.Rational;
 import com.drew.metadata.Metadata;
 import com.drew.tools.FileUtil;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-import java.text.SimpleDateFormat;
 import java.util.*;
 
 import static org.junit.Assert.*;
@@ -38,51 +35,24 @@ import static org.junit.Assert.*;
  */
 public class XmpReaderTest
 {
-    public static XmpDirectory processApp1Bytes(String filePath) throws IOException
-    {
-        Metadata metadata = new Metadata();
-        new XmpReader().extract(FileUtil.readBytes(filePath), metadata, JpegSegmentType.APP1);
-        XmpDirectory directory = metadata.getDirectory(XmpDirectory.class);
-        assertNotNull(directory);
-        return directory;
-    }
-
     private XmpDirectory _directory;
 
     @Before
     public void setUp() throws Exception
     {
-        _directory = processApp1Bytes("Tests/Data/withXmpAndIptc.jpg.app1.1");
-    }
+        Metadata metadata = new Metadata();
+        List<byte[]> jpegSegments = new ArrayList<byte[]>();
+        jpegSegments.add(FileUtil.readBytes("Tests/Data/withXmpAndIptc.jpg.app1.1"));
+        new XmpReader().readJpegSegments(jpegSegments, metadata, JpegSegmentType.APP1);
 
-    /*
-    [Xmp] Lens Information = 24/1 70/1 0/0 0/0
-    [Xmp] Lens = EF24-70mm f/2.8L USM
-    [Xmp] Serial Number = 380319450
-    [Xmp] Firmware = 1.2.1
-    [Xmp] Make = Canon
-    [Xmp] Model = Canon EOS 7D
-    [Xmp] Exposure Time = 1/125 sec
-    [Xmp] Exposure Program = Manual control
-    [Xmp] Aperture Value = F11
-    [Xmp] F-Number = F11
-    [Xmp] Focal Length = 57.0 mm
-    [Xmp] Shutter Speed Value = 1/124 sec
-    [Xmp] Date/Time Original = Sun Dec 12 11:41:35 GMT 2010
-    [Xmp] Date/Time Digitized = Sun Dec 12 11:41:35 GMT 2010
-    */
+        Collection<XmpDirectory> xmpDirectories = metadata.getDirectoriesOfType(XmpDirectory.class);
 
-    @Test
-    public void testExtract_LensInformation() throws Exception
-    {
-        // Note that this tag really holds a rational array, but XmpReader doesn't parse arrays
-        assertEquals("24/1 70/1 0/0 0/0", _directory.getString(XmpDirectory.TAG_LENS_INFO));
+        assertNotNull(xmpDirectories);
+        assertEquals(1, xmpDirectories.size());
 
-//        Rational[] info = _directory.getRationalArray(XmpDirectory.TAG_LENS_INFO);
-//        assertEquals(new Rational(24, 1), info[0]);
-//        assertEquals(new Rational(70, 1), info[1]);
-//        assertEquals(new Rational(0, 0), info[2]);
-//        assertEquals(new Rational(0, 0), info[3]);
+        _directory = xmpDirectories.iterator().next();
+
+        assertFalse(_directory.hasErrors());
     }
 
     @Test
@@ -92,123 +62,9 @@ public class XmpReaderTest
     }
 
     @Test
-    public void testExtract_Lens() throws Exception
-    {
-        assertEquals("EF24-70mm f/2.8L USM", _directory.getString(XmpDirectory.TAG_LENS));
-    }
-
-/*
-    // this requires further research
-
-    @Test
-    public void testExtract_Format() throws Exception
-    {
-        assertEquals("image/tiff", _directory.getString(XmpDirectory.TAG_FORMAT));
-    }
-
-    @Test
-    public void testExtract_Creator() throws Exception
-    {
-        assertEquals("", _directory.getString(XmpDirectory.TAG_CREATOR));
-    }
-
-    @Test
-    public void testExtract_Rights() throws Exception
-    {
-        assertEquals("", _directory.getString(XmpDirectory.TAG_RIGHTS));
-    }
-
-    @Test
-    public void testExtract_Description() throws Exception
-    {
-        assertEquals("", _directory.getString(XmpDirectory.TAG_DESCRIPTION));
-    }
-*/
-
-    @Test
-    public void testExtract_SerialNumber() throws Exception
-    {
-        assertEquals("380319450", _directory.getString(XmpDirectory.TAG_CAMERA_SERIAL_NUMBER));
-    }
-
-    @Test
-    public void testExtract_Firmware() throws Exception
-    {
-        assertEquals("1.2.1", _directory.getString(XmpDirectory.TAG_FIRMWARE));
-    }
-
-    @Test
-    public void testExtract_Maker() throws Exception
+    public void testExtract_PropertyCount() throws Exception
     {
-        assertEquals("Canon", _directory.getString(XmpDirectory.TAG_MAKE));
-    }
-
-    @Test
-    public void testExtract_Model() throws Exception
-    {
-        assertEquals("Canon EOS 7D", _directory.getString(XmpDirectory.TAG_MODEL));
-    }
-
-    @Test
-    public void testExtract_ExposureTime() throws Exception
-    {
-        // Note XmpReader doesn't parse this as a rational even though it appears to be... need more examples
-        assertEquals("1/125", _directory.getString(XmpDirectory.TAG_EXPOSURE_TIME));
-//        assertEquals(new Rational(1, 125), _directory.getRational(XmpDirectory.TAG_EXPOSURE_TIME));
-    }
-
-    @Test
-    public void testExtract_ExposureProgram() throws Exception
-    {
-        assertEquals(1, _directory.getInt(XmpDirectory.TAG_EXPOSURE_PROGRAM));
-    }
-
-    @Test
-    public void testExtract_FNumber() throws Exception
-    {
-        assertEquals(new Rational(11, 1), _directory.getRational(XmpDirectory.TAG_F_NUMBER));
-    }
-
-    @Test
-    public void testExtract_FocalLength() throws Exception
-    {
-        assertEquals(new Rational(57, 1), _directory.getRational(XmpDirectory.TAG_FOCAL_LENGTH));
-    }
-
-    @Test
-    public void testExtract_ShutterSpeed() throws Exception
-    {
-        assertEquals(new Rational(6965784, 1000000), _directory.getRational(XmpDirectory.TAG_SHUTTER_SPEED));
-    }
-
-    @Test
-    public void testExtract_OriginalDateTime() throws Exception
-    {
-        final Date actual = _directory.getDate(XmpDirectory.TAG_DATETIME_ORIGINAL);
-
-        // Underlying string value (in XMP data) is: 2010-12-12T12:41:35.00+01:00
-
-        assertEquals(new SimpleDateFormat("hh:mm:ss dd MM yyyy Z").parse("11:41:35 12 12 2010 +0000"), actual);
-//        assertEquals(new SimpleDateFormat("HH:mm:ss dd MMM yyyy Z").parse("12:41:35 12 Dec 2010 +0100"), actual);
-
-        Calendar calendar = new GregorianCalendar(2010, 12-1, 12, 11, 41, 35);
-        calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
-        assertEquals(calendar.getTime(), actual);
-    }
-
-    @Test
-    public void testExtract_DigitizedDateTime() throws Exception
-    {
-        final Date actual = _directory.getDate(XmpDirectory.TAG_DATETIME_DIGITIZED);
-
-        // Underlying string value (in XMP data) is: 2010-12-12T12:41:35.00+01:00
-
-        assertEquals(new SimpleDateFormat("hh:mm:ss dd MM yyyy Z").parse("11:41:35 12 12 2010 +0000"), actual);
-//        assertEquals(new SimpleDateFormat("HH:mm:ss dd MMM yyyy Z").parse("12:41:35 12 Dec 2010 +0100"), actual);
-
-        Calendar calendar = new GregorianCalendar(2010, 12-1, 12, 11, 41, 35);
-        calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
-        assertEquals(calendar.getTime(), actual);
+        assertEquals(179, _directory.getInt(XmpDirectory.TAG_XMP_VALUE_COUNT));
     }
 
     @Test
diff --git a/Tests/com/drew/testing/TestHelper.java b/Tests/com/drew/testing/TestHelper.java
index 77dce35..d17edd2 100644
--- a/Tests/com/drew/testing/TestHelper.java
+++ b/Tests/com/drew/testing/TestHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2015 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
  *    you may not use this file except in compliance with the License.
diff --git a/build.xml b/build.xml
deleted file mode 100644
index ae33499..0000000
--- a/build.xml
+++ /dev/null
@@ -1,192 +0,0 @@
-<?xml version="1.0"?>
-
-<!--
-  ~ Copyright 2002-2015 Drew Noakes
-  ~
-  ~    Licensed under the Apache License, Version 2.0 (the "License");
-  ~    you may not use this file except in compliance with the License.
-  ~    You may obtain a copy of the License at
-  ~
-  ~        http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~    Unless required by applicable law or agreed to in writing, software
-  ~    distributed under the License is distributed on an "AS IS" BASIS,
-  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~    See the License for the specific language governing permissions and
-  ~    limitations under the License.
-  ~
-  ~ More information about this project is available at:
-  ~
-  ~    https://drewnoakes.com/code/exif/
-  ~    https://github.com/drewnoakes/metadata-extractor
-  -->
-
-<!--suppress XmlUnboundNsPrefix -->
-<project name="metadata-extractor" default="test" basedir=".">
-
-    <description>metadata-extractor build file</description>
-
-    <property name="library-version"      value="2.7.2"/>
-    <property name="java-version"         value="1.5"/>
-    <property name="dist"                 location="Releases"/>
-    <property name="src"                  value="Source"/>
-    <property name="output"               value="Output/Source"/>
-    <property name="test-src"             value="Tests"/>
-    <property name="test-output"          value="Output/Tests"/>
-    <property name="sample-src"           value="Samples"/>
-    <property name="javadoc"              value="../javadoc/${library-version}"/>
-    <property name="sample-images"        value="../sample-images/"/>
-    <property name="sample-images-output" value="../sample-images/metadata"/>
-    <property name="lib"                  value="Libraries"/>
-    <property name="verbose"              value="true"/>
-    <property name="debug"                value="off"/>
-    <property name="lib-xmp"              value="${lib}/xmpcore-5.1.2.jar"/>
-    <property name="lib-junit"            value="${lib}/junit-4.11.jar"/>
-    <property name="classpath"            value="${lib-junit};${lib-xmp}"/>
-
-    <target name="clean" description="deletes and recreates the destination directory">
-        <delete verbose="${verbose}" dir="${output}"/>
-        <mkdir dir="${output}"/>
-        <delete verbose="${verbose}" dir="${test-output}"/>
-        <mkdir dir="${test-output}"/>
-        <mkdir dir="${dist}"/>
-    </target>
-
-    <target name="compile" description="compile the source">
-        <echo message="Using Java version ${ant.java.version}"/>
-        <javac classpath="${classpath}"
-               srcdir="${src}"
-               destdir="${output}"
-               source="${java-version}"
-               target="${java-version}"
-               encoding="UTF-8"
-               debug="${debug}"
-               verbose="${verbose}"
-               includeantruntime="false"/>
-        <javac classpath="${classpath};${output}"
-               srcdir="${test-src}"
-               destdir="${test-output}"
-               source="${java-version}"
-               target="${java-version}"
-               encoding="UTF-8"
-               debug="${debug}"
-               verbose="${verbose}"
-               includeantruntime="false"/>
-    </target>
-
-    <target name="test" depends="clean, compile" description="run all junit tests">
-        <junit printsummary="yes" logfailedtests="true" fork="yes" haltonfailure="yes" forkmode="once">
-            <formatter type="plain" usefile="false" />
-            <classpath>
-                <pathelement location="${output}"/>
-                <pathelement location="${test-output}"/>
-                <pathelement path="${java.class.path}"/>
-                <pathelement path="${lib-junit}"/>
-                <pathelement path="${lib-xmp}"/>
-            </classpath>
-            <batchtest>
-				<fileset dir="${test-src}">
-					<include name="**/*Test.java" />
-				</fileset>
-			</batchtest>
-        </junit>
-    </target>
-
-    <target name="process-sample-files" depends="compile" description="extract metadata from all sample images, and update output text files">
-        <delete verbose="${verbose}" dir="${sample-images-output}"/>
-        <mkdir dir="${sample-images-output}"/>
-        <java classname="com.drew.tools.ProcessAllImagesInFolderUtility"
-              classpath="${output};${lib-xmp}">
-            <arg value="${sample-images}"/>
-            <arg value="-text"/>
-        </java>
-        <java classname="com.drew.tools.ProcessAllImagesInFolderUtility"
-              classpath="${output};${lib-xmp}">
-            <arg value="${sample-images}"/>
-            <arg value="-wiki"/>
-        </java>
-    </target>
-
-    <target name="dist-binaries" depends="clean, compile, test" description="generate binary distribution">
-        <property name="bin-jar" value="${dist}/metadata-extractor-${library-version}.jar" />
-        <property name="bin-zip" value="${dist}/metadata-extractor-${library-version}.zip" />
-        <jar destfile="${bin-jar}" update="false">
-            <manifest>
-                <attribute name="Main-Class" value="com.drew.imaging.ImageMetadataReader"/>
-                <attribute name="Implementation-Title" value="metadata-extractor"/>
-                <attribute name="Implementation-Vendor" value="Drew Noakes"/>
-                <attribute name="Implementation-Version" value="${library-version}"/>
-            </manifest>
-            <fileset dir="${output}">
-                <exclude name="com/drew/tools" />
-                <exclude name="com/drew/tools/*.*" />
-            </fileset>
-            <file file="LICENSE-2.0.txt" />
-            <file file="README.txt" />
-        </jar>
-        <zip file="${bin-zip}" comment="Metadata Extractor ${library-version} - https://drewnoakes.com/code/exif/">
-            <file file="${bin-jar}" />
-            <file file="${lib-xmp}" />
-            <file file="LICENSE-2.0.txt" />
-            <file file="README.txt" />
-        </zip>
-        <delete file="${bin-jar}" />
-    </target>
-
-    <target name="dist-source" depends="clean, compile, test" description="generate source distribution">
-        <jar destfile="${dist}/metadata-extractor-${library-version}-src.jar" update="false">
-            <fileset dir="${src}">
-                <include name="**/*.*" />
-                <exclude name="com/drew/tools" />
-                <exclude name="com/drew/tools/*.*" />
-            </fileset>
-            <fileset dir=".">
-                <include name="LICENSE-2.0.txt" />
-                <include name="README.txt" />
-            </fileset>
-        </jar>
-    </target>
-
-    <target name="javadoc" description="generate javadoc documentation">
-        <delete verbose="${verbose}" dir="${javadoc}" />
-        <javadoc
-            destdir="${javadoc}"
-            defaultexcludes="yes"
-            author="true"
-            version="true"
-            use="true"
-            access="protected"
-            windowtitle="metadata-extractor - Javadoc - Extracts Exif, IPTC, XMP, ICC and other metadata from image files"
-            failonerror="true">
-            <arg value="-notimestamp" />
-            <!-- be sure to only use single quotes in the CDATA sections below -->
-            <!-- TODO include <link rel='shortcut icon' href='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor.ico' /> -->
-            <header><![CDATA[<a href='https://drewnoakes.com/code/exif/' title='Go to the project home page.'><img src='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-131x30.png' border="0" alt='Metadata Extractor Logo'></a>]]></header>
-            <bottom><![CDATA[<i>Copyright &#169; 2002-2015 Drew Noakes. All Rights Reserved.</i>
-<script src='http://www.google-analytics.com/urchin.js' type='text/javascript'></script>
-<script type='text/javascript'>
-_uacct = 'UA-936661-1';
-urchinTracker();
-</script>]]></bottom>
-
-            <!-- Only build Java -->
-            <packageset dir="${src}" defaultexcludes="yes">
-                <include name="com/**"/>
-                <exclude name="com/drew/tools/**"/>
-            </packageset>
-
-            <classpath>
-                <fileset dir=".">
-                    <include name="${lib-xmp}"/>
-                </fileset>
-            </classpath>
-
-        </javadoc>
-        <copy file="Resources/javadoc-stylesheet.css" tofile="${javadoc}/stylesheet.css" overwrite="yes" />
-    </target>
-
-    <target name="all" depends="dist-all, javadoc, process-sample-files" description="prepare source and binary distributions, and javadoc"/>
-
-    <target name="dist-all" depends="dist-source, dist-binaries" description="prepare source and binary distributions"/>
-
-</project>
diff --git a/pom.xml b/pom.xml
index bb89f0e..da678c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,19 +14,19 @@
 
     <groupId>com.drewnoakes</groupId>
     <artifactId>metadata-extractor</artifactId>
-    <version>2.7.2</version>
+    <version>2.10.1</version>
     <packaging>jar</packaging>
 
     <name>${project.groupId}:${project.artifactId}</name>
     <description>Java library for extracting EXIF, IPTC, XMP, ICC and other metadata from image files.</description>
-    
+
     <url>https://drewnoakes.com/code/exif/</url>
-    
+
     <issueManagement>
         <system>GitHub Issues</system>
         <url>https://github.com/drewnoakes/metadata-extractor/issues</url>
     </issueManagement>
-    
+
     <mailingLists>
         <mailingList>
             <name>Announce mailing list</name>
@@ -36,10 +36,6 @@
             <name>Development mailing list</name>
             <archive>http://groups.google.com/group/metadata-extractor-dev</archive>
         </mailingList>
-        <mailingList>
-            <name>Changes mailing list</name>
-            <archive>http://groups.google.com/group/metadata-extractor-changes</archive>
-        </mailingList>
     </mailingLists>
 
     <licenses>
@@ -72,16 +68,28 @@
         <dependency>
             <groupId>com.adobe.xmp</groupId>
             <artifactId>xmpcore</artifactId>
-            <version>5.1.2</version>
+            <version>5.1.3</version>
         </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.11</version>
+            <version>4.12</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
 
+    <profiles>
+        <profile>
+            <id>java8-doclint-disabled</id>
+            <activation>
+                <jdk>[1.8,)</jdk>
+            </activation>
+            <properties>
+                <javadoc.opts>-Xdoclint:none</javadoc.opts>
+            </properties>
+        </profile>
+    </profiles>
+
     <build>
         <directory>Output/maven</directory>
         <outputDirectory>Output/maven/classes</outputDirectory>
@@ -96,7 +104,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-source-plugin</artifactId>
-                <version>2.4</version>
+                <version>3.0.1</version>
                 <executions>
                     <execution>
                         <id>attach-sources</id>
@@ -109,20 +117,32 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.2</version>
+                <version>3.6.1</version>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.19.1</version>
                 <configuration>
-                  <source>1.5</source>
-                  <target>1.5</target>
+                    <includes>
+                        <include>**/*Test*.java</include>
+                    </includes>
                 </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-jar-plugin</artifactId>
-                <version>2.4</version>
+                <version>3.0.2</version>
                 <configuration>
                     <archive>
                         <manifest>
                             <addClasspath>true</addClasspath>
+                            <mainClass>com.drew.imaging.ImageMetadataReader</mainClass>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                         </manifest>
                         <manifestEntries>
                             <Implementation-Title>metadata-extractor</Implementation-Title>
@@ -135,15 +155,18 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.10.1</version>
+                <version>2.10.4</version>
                 <configuration>
-                    <!--<additionalparam>-Xdoclint:none</additionalparam>-->
+                    <additionalparam>${javadoc.opts}</additionalparam>
                     <stylesheetfile>${basedir}/src/main/javadoc/stylesheet.css</stylesheetfile>
                     <show>public</show>
-                    <windowtitle>metadata-extractor - Javadoc - Extracts Exif, IPTC, XMP, ICC and other metadata from image files</windowtitle>
+                    <windowtitle>metadata-extractor - Javadoc - Extracts Exif, IPTC, XMP, ICC and other metadata from
+                        image files
+                    </windowtitle>
                     <notimestamp>true</notimestamp>
-                    <header><![CDATA[<a href='https://drewnoakes.com/code/exif/' title='Go to the project home page.'><img src='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-131x30.png' border="0" alt='Metadata Extractor Logo'></a>]]></header>
-                    <bottom><![CDATA[<i>Copyright &#169; 2002-2015 Drew Noakes. All Rights Reserved.</i>
+                    <header>
+                        <![CDATA[<a href='https://drewnoakes.com/code/exif/' title='Go to the project home page.'><img src='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-131x30.png' border="0" alt='Metadata Extractor Logo'></a>]]></header>
+                    <bottom><![CDATA[<i>Copyright &#169; 2002-2017 Drew Noakes. All Rights Reserved.</i>
 <script src='http://www.google-analytics.com/urchin.js' type='text/javascript'></script>
 <script type='text/javascript'>
 _uacct = 'UA-936661-1';
@@ -164,7 +187,7 @@ urchinTracker();
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-gpg-plugin</artifactId>
-                <version>1.5</version>
+                <version>1.6</version>
                 <executions>
                     <execution>
                         <id>sign-artifacts</id>
@@ -178,7 +201,7 @@ urchinTracker();
             <plugin>
                 <groupId>org.sonatype.plugins</groupId>
                 <artifactId>nexus-staging-maven-plugin</artifactId>
-                <version>1.6.3</version>
+                <version>1.6.7</version>
                 <extensions>true</extensions>
                 <configuration>
                     <serverId>ossrh</serverId>
-- 
GitLab