Index: trunk/src/com/drew/metadata/Directory.java
===================================================================
--- trunk/src/com/drew/metadata/Directory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/Directory.java	(revision 8243)
@@ -81,4 +81,12 @@
 
 // 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();
+    }
 
     /**
@@ -759,4 +767,7 @@
                     if (timeZone != null)
                         parser.setTimeZone(timeZone);
+                    else
+                        parser.setTimeZone(TimeZone.getTimeZone("GMT")); // don't interpret zone time
+
                     return parser.parse(dateString);
                 } catch (ParseException ex) {
Index: trunk/src/com/drew/metadata/Metadata.java
===================================================================
--- trunk/src/com/drew/metadata/Metadata.java	(revision 8132)
+++ trunk/src/com/drew/metadata/Metadata.java	(revision 8243)
@@ -37,26 +37,25 @@
 {
     @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.
-     */
-    @NotNull
-    private final Collection<Directory> _directoryList = new ArrayList<Directory>();
-
-    /**
-     * Returns an objects for iterating over Directory objects in the order in which they were added.
-     *
-     * @return an iterable collection of directories
+    private final Map<Class<? extends Directory>,Collection<Directory>> _directoryListByClass = new HashMap<Class<? extends Directory>, Collection<Directory>>();
+
+    /**
+     * Returns an iterable set of the {@link Directory} instances contained in this metadata collection.
+     *
+     * @return an iterable set of directories
      */
     @NotNull
     public Iterable<Directory> getDirectories()
     {
-        return Collections.unmodifiableCollection(_directoryList);
-    }
-
-    /**
-     * Returns a count of unique directories in this metadata collection.
+        return new DirectoryIterable(_directoryListByClass);
+    }
+
+    @Nullable
+    public <T extends Directory> Collection<T> getDirectoriesOfType(Class<T> type)
+    {
+        return (Collection<T>)_directoryListByClass.get(type);
+    }
+
+    /**
+     * Returns the count of directories in this metadata collection.
      *
      * @return the number of unique directory types set for this metadata collection
@@ -64,67 +63,53 @@
     public int getDirectoryCount()
     {
-        return _directoryList.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.
-     *
-     * @param type the type of the Directory implementation required.
-     * @return a directory of the specified type.
-     */
-    @NotNull
+        int count = 0;
+        for (Map.Entry<Class<? extends Directory>,Collection<Directory>> pair : _directoryListByClass.entrySet())
+            count += pair.getValue().size();
+        return count;
+    }
+
+    /**
+     * Adds a directory to this metadata collection.
+     *
+     * @param directory the {@link Directory} to add into this metadata collection.
+     */
+    public <T extends Directory> void addDirectory(@NotNull T directory)
+    {
+        getOrCreateDirectoryList(directory.getClass()).add(directory);
+    }
+
+    /**
+     * Gets the first {@link Directory} of the specified type contained within this metadata collection.
+     * If no instances of this type are present, <code>null</code> is returned.
+     *
+     * @param type the Directory type
+     * @param <T> the Directory type
+     * @return the first Directory of type T in this metadata collection, or <code>null</code> if none exist
+     */
+    @Nullable
     @SuppressWarnings("unchecked")
-    public <T extends Directory> T getOrCreateDirectory(@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.
 
-        // 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;
-    }
-
-    /**
-     * If this {@link Metadata} object contains a {@link Directory} of the specified type, it is returned.
-     * Otherwise <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>.
-     */
-    @Nullable
-    @SuppressWarnings("unchecked")
-    public <T extends Directory> T getDirectory(@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);
-    }
-
-    /**
-     * Indicates whether a given directory type has been created in this metadata
-     * repository.  Directories are created by calling {@link Metadata#getOrCreateDirectory(Class)}.
+        Collection<Directory> list = getDirectoryList(type);
+
+        if (list == null || list.isEmpty())
+            return null;
+
+        return (T)list.iterator().next();
+    }
+
+    /**
+     * Indicates whether an instance of the given directory type exists in this Metadata instance.
      *
      * @param type the {@link Directory} type
-     * @return true if the {@link Directory} has been created
-     */
-    public boolean containsDirectory(Class<? extends Directory> type)
-    {
-        return _directoryByClass.containsKey(type);
+     * @return <code>true</code> if a {@link Directory} of the specified type exists, otherwise <code>false</code>
+     */
+    public boolean containsDirectoryOfType(Class<? extends Directory> type)
+    {
+        Collection<Directory> list = getDirectoryList(type);
+        return list != null && !list.isEmpty();
     }
 
@@ -137,5 +122,5 @@
     public boolean hasErrors()
     {
-        for (Directory directory : _directoryList) {
+        for (Directory directory : getDirectories()) {
             if (directory.hasErrors())
                 return true;
@@ -147,9 +132,79 @@
     public String toString()
     {
+        int count = getDirectoryCount();
         return String.format("Metadata (%d %s)",
-            _directoryList.size(),
-            _directoryList.size() == 1
+            count,
+            count == 1
                 ? "directory"
                 : "directories");
     }
+
+    @Nullable
+    private <T extends Directory> Collection<Directory> getDirectoryList(@NotNull Class<T> type)
+    {
+        return _directoryListByClass.get(type);
+    }
+
+    @NotNull
+    private <T extends Directory> Collection<Directory> getOrCreateDirectoryList(@NotNull Class<T> type)
+    {
+        Collection<Directory> collection = getDirectoryList(type);
+        if (collection != null)
+            return collection;
+        collection = new ArrayList<Directory>();
+        _directoryListByClass.put(type, collection);
+        return collection;
+    }
+
+    private static class DirectoryIterable implements Iterable<Directory>
+    {
+        private final Map<Class<? extends Directory>, Collection<Directory>> _map;
+
+        public DirectoryIterable(Map<Class<? extends Directory>, Collection<Directory>> map)
+        {
+            _map = map;
+        }
+
+        public Iterator<Directory> iterator()
+        {
+            return new DirectoryIterator(_map);
+        }
+
+        private static class DirectoryIterator implements Iterator<Directory>
+        {
+            @NotNull
+            private final Iterator<Map.Entry<Class<? extends Directory>, Collection<Directory>>> _mapIterator;
+            @Nullable
+            private Iterator<Directory> _listIterator;
+
+            public DirectoryIterator(Map<Class<? extends Directory>, Collection<Directory>> map)
+            {
+                _mapIterator = map.entrySet().iterator();
+
+                if (_mapIterator.hasNext())
+                    _listIterator = _mapIterator.next().getValue().iterator();
+            }
+
+            public boolean hasNext()
+            {
+                return _listIterator != null && (_listIterator.hasNext() || _mapIterator.hasNext());
+            }
+
+            public Directory next()
+            {
+                if (_listIterator == null || (!_listIterator.hasNext() && !_mapIterator.hasNext()))
+                    throw new NoSuchElementException();
+
+                while (!_listIterator.hasNext())
+                    _listIterator = _mapIterator.next().getValue().iterator();
+
+                return _listIterator.next();
+            }
+
+            public void remove()
+            {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 8243)
+++ trunk/src/com/drew/metadata/exif/ExifDescriptorBase.java	(revision 8243)
@@ -0,0 +1,1105 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.imaging.PhotographicConversions;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.UnsupportedEncodingException;
+import java.text.DecimalFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.drew.metadata.exif.ExifDirectoryBase.*;
+
+/**
+ * Base class for several Exif format descriptor classes.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class ExifDescriptorBase<T extends Directory> extends TagDescriptor<T>
+{
+    /**
+     * Dictates whether rational values will be represented in decimal format in instances
+     * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3).
+     */
+    private final boolean _allowDecimalRepresentationOfRationals = true;
+
+    @NotNull
+    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
+    @NotNull
+    private static final java.text.DecimalFormat SimpleDecimalFormatterWithPrecision = new DecimalFormat("0.0");
+
+    // Note for the potential addition of brightness presentation in eV:
+    // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv),
+    // you must add SensitivityValue(Sv).
+    // Ev=BV+Sv   Sv=log2(ISOSpeedRating/3.125)
+    // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32.
+
+    public ExifDescriptorBase(@NotNull T directory)
+    {
+        super(directory);
+    }
+
+    @Nullable
+    @Override
+    public String getDescription(int tagType)
+    {
+        // TODO order case blocks and corresponding methods in the same order as the TAG_* values are defined
+
+        switch (tagType) {
+            case TAG_INTEROP_INDEX:
+                return getInteropIndexDescription();
+            case TAG_INTEROP_VERSION:
+                return getInteropVersionDescription();
+            case TAG_ORIENTATION:
+                return getOrientationDescription();
+            case TAG_RESOLUTION_UNIT:
+                return getResolutionDescription();
+            case TAG_YCBCR_POSITIONING:
+                return getYCbCrPositioningDescription();
+            case TAG_X_RESOLUTION:
+                return getXResolutionDescription();
+            case TAG_Y_RESOLUTION:
+                return getYResolutionDescription();
+            case TAG_IMAGE_WIDTH:
+                return getImageWidthDescription();
+            case TAG_IMAGE_HEIGHT:
+                return getImageHeightDescription();
+            case TAG_BITS_PER_SAMPLE:
+                return getBitsPerSampleDescription();
+            case TAG_PHOTOMETRIC_INTERPRETATION:
+                return getPhotometricInterpretationDescription();
+            case TAG_ROWS_PER_STRIP:
+                return getRowsPerStripDescription();
+            case TAG_STRIP_BYTE_COUNTS:
+                return getStripByteCountsDescription();
+            case TAG_SAMPLES_PER_PIXEL:
+                return getSamplesPerPixelDescription();
+            case TAG_PLANAR_CONFIGURATION:
+                return getPlanarConfigurationDescription();
+            case TAG_YCBCR_SUBSAMPLING:
+                return getYCbCrSubsamplingDescription();
+            case TAG_REFERENCE_BLACK_WHITE:
+                return getReferenceBlackWhiteDescription();
+            case TAG_WIN_AUTHOR:
+                return getWindowsAuthorDescription();
+            case TAG_WIN_COMMENT:
+                return getWindowsCommentDescription();
+            case TAG_WIN_KEYWORDS:
+                return getWindowsKeywordsDescription();
+            case TAG_WIN_SUBJECT:
+                return getWindowsSubjectDescription();
+            case TAG_WIN_TITLE:
+                return getWindowsTitleDescription();
+            case TAG_NEW_SUBFILE_TYPE:
+                return getNewSubfileTypeDescription();
+            case TAG_SUBFILE_TYPE:
+                return getSubfileTypeDescription();
+            case TAG_THRESHOLDING:
+                return getThresholdingDescription();
+            case TAG_FILL_ORDER:
+                return getFillOrderDescription();
+            case TAG_EXPOSURE_TIME:
+                return getExposureTimeDescription();
+            case TAG_SHUTTER_SPEED:
+                return getShutterSpeedDescription();
+            case TAG_FNUMBER:
+                return getFNumberDescription();
+            case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
+                return getCompressedAverageBitsPerPixelDescription();
+            case TAG_SUBJECT_DISTANCE:
+                return getSubjectDistanceDescription();
+            case TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case TAG_FLASH:
+                return getFlashDescription();
+            case TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case TAG_COLOR_SPACE:
+                return getColorSpaceDescription();
+            case TAG_EXIF_IMAGE_WIDTH:
+                return getExifImageWidthDescription();
+            case TAG_EXIF_IMAGE_HEIGHT:
+                return getExifImageHeightDescription();
+            case TAG_FOCAL_PLANE_RESOLUTION_UNIT:
+                return getFocalPlaneResolutionUnitDescription();
+            case TAG_FOCAL_PLANE_X_RESOLUTION:
+                return getFocalPlaneXResolutionDescription();
+            case TAG_FOCAL_PLANE_Y_RESOLUTION:
+                return getFocalPlaneYResolutionDescription();
+            case TAG_EXPOSURE_PROGRAM:
+                return getExposureProgramDescription();
+            case TAG_APERTURE:
+                return getApertureValueDescription();
+            case TAG_MAX_APERTURE:
+                return getMaxApertureValueDescription();
+            case TAG_SENSING_METHOD:
+                return getSensingMethodDescription();
+            case TAG_EXPOSURE_BIAS:
+                return getExposureBiasDescription();
+            case TAG_FILE_SOURCE:
+                return getFileSourceDescription();
+            case TAG_SCENE_TYPE:
+                return getSceneTypeDescription();
+            case TAG_COMPONENTS_CONFIGURATION:
+                return getComponentConfigurationDescription();
+            case TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case TAG_FLASHPIX_VERSION:
+                return getFlashPixVersionDescription();
+            case TAG_ISO_EQUIVALENT:
+                return getIsoEquivalentDescription();
+            case TAG_USER_COMMENT:
+                return getUserCommentDescription();
+            case TAG_CUSTOM_RENDERED:
+                return getCustomRenderedDescription();
+            case TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case TAG_WHITE_BALANCE_MODE:
+                return getWhiteBalanceModeDescription();
+            case TAG_DIGITAL_ZOOM_RATIO:
+                return getDigitalZoomRatioDescription();
+            case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
+                return get35mmFilmEquivFocalLengthDescription();
+            case TAG_SCENE_CAPTURE_TYPE:
+                return getSceneCaptureTypeDescription();
+            case TAG_GAIN_CONTROL:
+                return getGainControlDescription();
+            case TAG_CONTRAST:
+                return getContrastDescription();
+            case TAG_SATURATION:
+                return getSaturationDescription();
+            case TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case TAG_SUBJECT_DISTANCE_RANGE:
+                return getSubjectDistanceRangeDescription();
+            case TAG_SENSITIVITY_TYPE:
+                return getSensitivityTypeRangeDescription();
+            case TAG_COMPRESSION:
+                return getCompressionDescription();
+            case TAG_JPEG_PROC:
+                return getJpegProcDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getInteropVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_INTEROP_VERSION, 2);
+    }
+
+    @Nullable
+    public String getInteropIndexDescription()
+    {
+        String value = _directory.getString(TAG_INTEROP_INDEX);
+
+        if (value == null)
+            return null;
+
+        return "R98".equalsIgnoreCase(value.trim())
+            ? "Recommended Exif Interoperability Rules (ExifR98)"
+            : "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getReferenceBlackWhiteDescription()
+    {
+        int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE);
+        if (ints==null || ints.length < 6)
+            return null;
+        int blackR = ints[0];
+        int whiteR = ints[1];
+        int blackG = ints[2];
+        int whiteG = ints[3];
+        int blackB = ints[4];
+        int whiteB = ints[5];
+        return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB);
+    }
+
+    @Nullable
+    public String getYResolutionDescription()
+    {
+        Rational value = _directory.getRational(TAG_Y_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getXResolutionDescription()
+    {
+        Rational value = _directory.getRational(TAG_X_RESOLUTION);
+        if (value == null)
+            return null;
+        final String unit = getResolutionDescription();
+        return String.format("%s dots per %s",
+            value.toSimpleString(_allowDecimalRepresentationOfRationals),
+            unit == null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getYCbCrPositioningDescription()
+    {
+        return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point");
+    }
+
+    @Nullable
+    public String getOrientationDescription()
+    {
+        return getIndexedDescription(TAG_ORIENTATION, 1,
+            "Top, left side (Horizontal / normal)",
+            "Top, right side (Mirror horizontal)",
+            "Bottom, right side (Rotate 180)",
+            "Bottom, left side (Mirror vertical)",
+            "Left side, top (Mirror horizontal and rotate 270 CW)",
+            "Right side, top (Rotate 90 CW)",
+            "Right side, bottom (Mirror horizontal and rotate 90 CW)",
+            "Left side, bottom (Rotate 270 CW)");
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm");
+    }
+
+    /** The Windows specific tags uses plain Unicode. */
+    @Nullable
+    private String getUnicodeDescription(int tag)
+    {
+        byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null)
+            return null;
+        try {
+            // Decode the unicode string and trim the unicode zero "\0" from the end.
+            return new String(bytes, "UTF-16LE").trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getWindowsAuthorDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_AUTHOR);
+    }
+
+    @Nullable
+    public String getWindowsCommentDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_COMMENT);
+    }
+
+    @Nullable
+    public String getWindowsKeywordsDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_KEYWORDS);
+    }
+
+    @Nullable
+    public String getWindowsTitleDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_TITLE);
+    }
+
+    @Nullable
+    public String getWindowsSubjectDescription()
+    {
+        return getUnicodeDescription(TAG_WIN_SUBJECT);
+    }
+
+    @Nullable
+    public String getYCbCrSubsamplingDescription()
+    {
+        int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING);
+        if (positions == null || positions.length < 2)
+            return null;
+        if (positions[0] == 2 && positions[1] == 1) {
+            return "YCbCr4:2:2";
+        } else if (positions[0] == 2 && positions[1] == 2) {
+            return "YCbCr4:2:0";
+        } else {
+            return "(Unknown)";
+        }
+    }
+
+    @Nullable
+    public String getPlanarConfigurationDescription()
+    {
+        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
+        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
+        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
+        // plane format.
+        return getIndexedDescription(TAG_PLANAR_CONFIGURATION,
+            1,
+            "Chunky (contiguous for each subsampling pixel)",
+            "Separate (Y-plane/Cb-plane/Cr-plane format)"
+        );
+    }
+
+    @Nullable
+    public String getSamplesPerPixelDescription()
+    {
+        String value = _directory.getString(TAG_SAMPLES_PER_PIXEL);
+        return value == null ? null : value + " samples/pixel";
+    }
+
+    @Nullable
+    public String getRowsPerStripDescription()
+    {
+        final String value = _directory.getString(TAG_ROWS_PER_STRIP);
+        return value == null ? null : value + " rows/strip";
+    }
+
+    @Nullable
+    public String getStripByteCountsDescription()
+    {
+        final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS);
+        return value == null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getPhotometricInterpretationDescription()
+    {
+        // Shows the color space of the image data components
+        Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "WhiteIsZero";
+            case 1: return "BlackIsZero";
+            case 2: return "RGB";
+            case 3: return "RGB Palette";
+            case 4: return "Transparency Mask";
+            case 5: return "CMYK";
+            case 6: return "YCbCr";
+            case 8: return "CIELab";
+            case 9: return "ICCLab";
+            case 10: return "ITULab";
+            case 32803: return "Color Filter Array";
+            case 32844: return "Pixar LogL";
+            case 32845: return "Pixar LogLuv";
+            case 32892: return "Linear Raw";
+            default:
+                return "Unknown colour space";
+        }
+    }
+
+    @Nullable
+    public String getBitsPerSampleDescription()
+    {
+        String value = _directory.getString(TAG_BITS_PER_SAMPLE);
+        return value == null ? null : value + " bits/component/pixel";
+    }
+
+    @Nullable
+    public String getImageWidthDescription()
+    {
+        String value = _directory.getString(TAG_IMAGE_WIDTH);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getImageHeightDescription()
+    {
+        String value = _directory.getString(TAG_IMAGE_HEIGHT);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getNewSubfileTypeDescription()
+    {
+        return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page reduced-resolution image",
+            "Transparency mask",
+            "Transparency mask of reduced-resolution image",
+            "Transparency mask of multi-page image",
+            "Transparency mask of reduced-resolution multi-page image"
+        );
+    }
+
+    @Nullable
+    public String getSubfileTypeDescription()
+    {
+        return getIndexedDescription(TAG_SUBFILE_TYPE, 1,
+            "Full-resolution image",
+            "Reduced-resolution image",
+            "Single page of multi-page image"
+        );
+    }
+
+    @Nullable
+    public String getThresholdingDescription()
+    {
+        return getIndexedDescription(TAG_THRESHOLDING, 1,
+            "No dithering or halftoning",
+            "Ordered dither or halftone",
+            "Randomized dither"
+        );
+    }
+
+    @Nullable
+    public String getFillOrderDescription()
+    {
+        return getIndexedDescription(TAG_FILL_ORDER, 1,
+            "Normal",
+            "Reversed"
+        );
+    }
+
+    @Nullable
+    public String getSubjectDistanceRangeDescription()
+    {
+        return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE,
+            "Unknown",
+            "Macro",
+            "Close view",
+            "Distant view"
+        );
+    }
+
+    @Nullable
+    public String getSensitivityTypeRangeDescription()
+    {
+        return getIndexedDescription(TAG_SENSITIVITY_TYPE,
+            "Unknown",
+            "Standard Output Sensitivity",
+            "Recommended Exposure Index",
+            "ISO Speed",
+            "Standard Output Sensitivity and Recommended Exposure Index",
+            "Standard Output Sensitivity and ISO Speed",
+            "Recommended Exposure Index and ISO Speed",
+            "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed"
+        );
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        return getIndexedDescription(TAG_SHARPNESS,
+            "None",
+            "Low",
+            "Hard"
+        );
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        return getIndexedDescription(TAG_SATURATION,
+            "None",
+            "Low saturation",
+            "High saturation"
+        );
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        return getIndexedDescription(TAG_CONTRAST,
+            "None",
+            "Soft",
+            "Hard"
+        );
+    }
+
+    @Nullable
+    public String getGainControlDescription()
+    {
+        return getIndexedDescription(TAG_GAIN_CONTROL,
+            "None",
+            "Low gain up",
+            "Low gain down",
+            "High gain up",
+            "High gain down"
+        );
+    }
+
+    @Nullable
+    public String getSceneCaptureTypeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE,
+            "Standard",
+            "Landscape",
+            "Portrait",
+            "Night scene"
+        );
+    }
+
+    @Nullable
+    public String get35mmFilmEquivFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
+        return value == null
+            ? null
+            : value == 0
+            ? "Unknown"
+            : SimpleDecimalFormatter.format(value) + "mm";
+    }
+
+    @Nullable
+    public String getDigitalZoomRatioDescription()
+    {
+        Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO);
+        return value == null
+            ? null
+            : value.getNumerator() == 0
+            ? "Digital zoom not used."
+            : SimpleDecimalFormatter.format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getWhiteBalanceModeDescription()
+    {
+        return getIndexedDescription(TAG_WHITE_BALANCE_MODE,
+            "Auto white balance",
+            "Manual white balance"
+        );
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        return getIndexedDescription(TAG_EXPOSURE_MODE,
+            "Auto exposure",
+            "Manual exposure",
+            "Auto bracket"
+        );
+    }
+
+    @Nullable
+    public String getCustomRenderedDescription()
+    {
+        return getIndexedDescription(TAG_CUSTOM_RENDERED,
+            "Normal process",
+            "Custom process"
+        );
+    }
+
+    @Nullable
+    public String getUserCommentDescription()
+    {
+        byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT);
+        if (commentBytes == null)
+            return null;
+        if (commentBytes.length == 0)
+            return "";
+
+        final Map<String, String> encodingMap = new HashMap<String, String>();
+        encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
+        encodingMap.put("UNICODE", "UTF-16LE");
+        encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
+
+        try {
+            if (commentBytes.length >= 10) {
+                String firstTenBytesString = new String(commentBytes, 0, 10);
+
+                // try each encoding name
+                for (Map.Entry<String, String> pair : encodingMap.entrySet()) {
+                    String encodingName = pair.getKey();
+                    String charset = pair.getValue();
+                    if (firstTenBytesString.startsWith(encodingName)) {
+                        // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start
+                        for (int j = encodingName.length(); j < 10; j++) {
+                            byte b = commentBytes[j];
+                            if (b != '\0' && b != ' ')
+                                return new String(commentBytes, j, commentBytes.length - j, charset).trim();
+                        }
+                        return new String(commentBytes, 10, commentBytes.length - 10, charset).trim();
+                    }
+                }
+            }
+            // special handling fell through, return a plain string representation
+            return new String(commentBytes, System.getProperty("file.encoding")).trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getIsoEquivalentDescription()
+    {
+        // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values
+        Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT);
+        // There used to be a check here that multiplied ISO values < 50 by 200.
+        // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40.
+        return isoEquiv != null
+            ? Integer.toString(isoEquiv)
+            : null;
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_EXIF_VERSION, 2);
+    }
+
+    @Nullable
+    public String getFlashPixVersionDescription()
+    {
+        return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2);
+    }
+
+    @Nullable
+    public String getSceneTypeDescription()
+    {
+        return getIndexedDescription(TAG_SCENE_TYPE,
+            1,
+            "Directly photographed image"
+        );
+    }
+
+    @Nullable
+    public String getFileSourceDescription()
+    {
+        return getIndexedDescription(TAG_FILE_SOURCE,
+            1,
+            "Film Scanner",
+            "Reflection Print Scanner",
+            "Digital Still Camera (DSC)"
+        );
+    }
+
+    @Nullable
+    public String getExposureBiasDescription()
+    {
+        Rational value = _directory.getRational(TAG_EXPOSURE_BIAS);
+        if (value == null)
+            return null;
+        return value.toSimpleString(true) + " EV";
+    }
+
+    @Nullable
+    public String getMaxApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return "f/" + SimpleDecimalFormatterWithPrecision.format(fStop);
+    }
+
+    @Nullable
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(TAG_APERTURE);
+        if (aperture == null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return "f/" + SimpleDecimalFormatterWithPrecision.format(fStop);
+    }
+
+    @Nullable
+    public String getExposureProgramDescription()
+    {
+        return getIndexedDescription(TAG_EXPOSURE_PROGRAM,
+            1,
+            "Manual control",
+            "Program normal",
+            "Aperture priority",
+            "Shutter priority",
+            "Program creative (slow program)",
+            "Program action (high-speed program)",
+            "Portrait mode",
+            "Landscape mode"
+        );
+    }
+
+
+    @Nullable
+    public String getFocalPlaneXResolutionDescription()
+    {
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION);
+        if (rational == null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit == null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneYResolutionDescription()
+    {
+        Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION);
+        if (rational == null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit == null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneResolutionUnitDescription()
+    {
+        // Unit of FocalPlaneXResolution/FocalPlaneYResolution.
+        // '1' means no-unit, '2' inch, '3' centimeter.
+        return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+            1,
+            "(No unit)",
+            "Inches",
+            "cm"
+        );
+    }
+
+    @Nullable
+    public String getExifImageWidthDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getExifImageHeightDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT);
+        return value == null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        final Integer value = _directory.getInteger(TAG_COLOR_SPACE);
+        if (value == null)
+            return null;
+        if (value == 1)
+            return "sRGB";
+        if (value == 65535)
+            return "Undefined";
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Rational value = _directory.getRational(TAG_FOCAL_LENGTH);
+        if (value == null)
+            return null;
+        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " mm";
+    }
+
+    @Nullable
+    public String getFlashDescription()
+    {
+        /*
+         * This is a bit mask.
+         * 0 = flash fired
+         * 1 = return detected
+         * 2 = return able to be detected
+         * 3 = unknown
+         * 4 = auto used
+         * 5 = unknown
+         * 6 = red eye reduction used
+         */
+
+        final Integer value = _directory.getInteger(TAG_FLASH);
+
+        if (value == null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        if ((value & 0x1) != 0)
+            sb.append("Flash fired");
+        else
+            sb.append("Flash did not fire");
+
+        // check if we're able to detect a return, before we mention it
+        if ((value & 0x4) != 0) {
+            if ((value & 0x2) != 0)
+                sb.append(", return detected");
+            else
+                sb.append(", return not detected");
+        }
+
+        if ((value & 0x10) != 0)
+            sb.append(", auto");
+
+        if ((value & 0x40) != 0)
+            sb.append(", red-eye reduction");
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '4' flash,
+        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
+        // '21' D65, '22' D75, '255' other.
+        // see http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35
+        final Integer value = _directory.getInteger(TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Daylight";
+            case 2: return "Florescent";
+            case 3: return "Tungsten";
+            case 4: return "Flash";
+            case 9: return "Fine Weather";
+            case 10: return "Cloudy";
+            case 11: return "Shade";
+            case 12: return "Daylight Flourescent";
+            case 13: return "Day White Flourescent";
+            case 14: return "Cool White Flourescent";
+            case 15: return "White Flourescent";
+            case 16: return "Warm White Flourescent";
+            case 17: return "Standard light";
+            case 18: return "Standard light (B)";
+            case 19: return "Standard light (C)";
+            case 20: return "D55";
+            case 21: return "D65";
+            case 22: return "D75";
+            case 23: return "D50";
+            case 24: return "Studio Tungsten";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
+        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
+        Integer value = _directory.getInteger(TAG_METERING_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Average";
+            case 2: return "Center weighted average";
+            case 3: return "Spot";
+            case 4: return "Multi-spot";
+            case 5: return "Multi-segment";
+            case 6: return "Partial";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getCompressionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_COMPRESSION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Uncompressed";
+            case 2: return "CCITT 1D";
+            case 3: return "T4/Group 3 Fax";
+            case 4: return "T6/Group 4 Fax";
+            case 5: return "LZW";
+            case 6: return "JPEG (old-style)";
+            case 7: return "JPEG";
+            case 8: return "Adobe Deflate";
+            case 9: return "JBIG B&W";
+            case 10: return "JBIG Color";
+            case 99: return "JPEG";
+            case 262: return "Kodak 262";
+            case 32766: return "Next";
+            case 32767: return "Sony ARW Compressed";
+            case 32769: return "Packed RAW";
+            case 32770: return "Samsung SRW Compressed";
+            case 32771: return "CCIRLEW";
+            case 32772: return "Samsung SRW Compressed 2";
+            case 32773: return "PackBits";
+            case 32809: return "Thunderscan";
+            case 32867: return "Kodak KDC Compressed";
+            case 32895: return "IT8CTPAD";
+            case 32896: return "IT8LW";
+            case 32897: return "IT8MP";
+            case 32898: return "IT8BL";
+            case 32908: return "PixarFilm";
+            case 32909: return "PixarLog";
+            case 32946: return "Deflate";
+            case 32947: return "DCS";
+            case 34661: return "JBIG";
+            case 34676: return "SGILog";
+            case 34677: return "SGILog24";
+            case 34712: return "JPEG 2000";
+            case 34713: return "Nikon NEF Compressed";
+            case 34715: return "JBIG2 TIFF FX";
+            case 34718: return "Microsoft Document Imaging (MDI) Binary Level Codec";
+            case 34719: return "Microsoft Document Imaging (MDI) Progressive Transform Codec";
+            case 34720: return "Microsoft Document Imaging (MDI) Vector";
+            case 34892: return "Lossy JPEG";
+            case 65000: return "Kodak DCR Compressed";
+            case 65535: return "Pentax PEF Compressed";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSubjectDistanceDescription()
+    {
+        Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE);
+        if (value == null)
+            return null;
+        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " metres";
+    }
+
+    @Nullable
+    public String getCompressedAverageBitsPerPixelDescription()
+    {
+        Rational value = _directory.getRational(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);
+        if (value == null)
+            return null;
+        String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals);
+        return value.isInteger() && value.intValue() == 1
+            ? ratio + " bit/pixel"
+            : ratio + " bits/pixel";
+    }
+
+    @Nullable
+    public String getExposureTimeDescription()
+    {
+        String value = _directory.getString(TAG_EXPOSURE_TIME);
+        return value == null ? null : value + " sec";
+    }
+
+    @Nullable
+    public String getShutterSpeedDescription()
+    {
+        // I believe this method to now be stable, but am leaving some alternative snippets of
+        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
+
+//        float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+//        int apexPower = (int)Math.pow(2.0, apexValue);
+//        return "1/" + apexPower + " sec";
+        // TODO test this method
+        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
+        // description (spotted bug using a Canon EOS 300D)
+        // thanks also to Gli Blr for spotting this bug
+        Float apexValue = _directory.getFloatObject(TAG_SHUTTER_SPEED);
+        if (apexValue == null)
+            return null;
+        if (apexValue <= 1) {
+            float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2))));
+            long apexPower10 = Math.round((double)apexPower * 10.0);
+            float fApexPower = (float)apexPower10 / 10.0f;
+            return fApexPower + " sec";
+        } else {
+            int apexPower = (int)((Math.exp(apexValue * Math.log(2))));
+            return "1/" + apexPower + " sec";
+        }
+
+/*
+        // This alternative implementation offered by Bill Richards
+        // TODO determine which is the correct / more-correct implementation
+        double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+        double apexPower = Math.pow(2.0, apexValue);
+
+        StringBuffer sb = new StringBuffer();
+        if (apexPower > 1)
+            apexPower = Math.floor(apexPower);
+
+        if (apexPower < 1) {
+            sb.append((int)Math.round(1/apexPower));
+        } else {
+            sb.append("1/");
+            sb.append((int)apexPower);
+        }
+        sb.append(" sec");
+        return sb.toString();
+*/
+    }
+
+    @Nullable
+    public String getFNumberDescription()
+    {
+        Rational value = _directory.getRational(TAG_FNUMBER);
+        if (value == null)
+            return null;
+        return "f/" + SimpleDecimalFormatterWithPrecision.format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getSensingMethodDescription()
+    {
+        // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor
+        // '4' Three-chip color area sensor, '5' Color sequential area sensor
+        // '7' Trilinear sensor '8' Color sequential linear sensor,  'Other' reserved
+        return getIndexedDescription(TAG_SENSING_METHOD,
+            1,
+            "(Not defined)",
+            "One-chip color area sensor",
+            "Two-chip color area sensor",
+            "Three-chip color area sensor",
+            "Color sequential area sensor",
+            null,
+            "Trilinear sensor",
+            "Color sequential linear sensor"
+        );
+    }
+
+    @Nullable
+    public String getComponentConfigurationDescription()
+    {
+        int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION);
+        if (components == null)
+            return null;
+        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
+        StringBuilder componentConfig = new StringBuilder();
+        for (int i = 0; i < Math.min(4, components.length); i++) {
+            int j = components[i];
+            if (j > 0 && j < componentStrings.length) {
+                componentConfig.append(componentStrings[j]);
+            }
+        }
+        return componentConfig.toString();
+    }
+
+    @Nullable
+    public String getJpegProcDescription()
+    {
+        Integer value = _directory.getInteger(TAG_JPEG_PROC);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1: return "Baseline";
+            case 14: return "Lossless";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 8243)
+++ trunk/src/com/drew/metadata/exif/ExifDirectoryBase.java	(revision 8243)
@@ -0,0 +1,732 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Base class for several Exif format tag directories.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public abstract class ExifDirectoryBase extends Directory
+{
+    public static final int TAG_INTEROP_INDEX = 0x0001;
+    public static final int TAG_INTEROP_VERSION = 0x0002;
+
+    /**
+     * The new subfile type tag.
+     * 0 = Full-resolution Image
+     * 1 = Reduced-resolution image
+     * 2 = Single page of multi-page image
+     * 3 = Single page of multi-page reduced-resolution image
+     * 4 = Transparency mask
+     * 5 = Transparency mask of reduced-resolution image
+     * 6 = Transparency mask of multi-page image
+     * 7 = Transparency mask of reduced-resolution multi-page image
+     */
+    public static final int TAG_NEW_SUBFILE_TYPE                  = 0x00FE;
+    /**
+     * The old subfile type tag.
+     * 1 = Full-resolution image (Main image)
+     * 2 = Reduced-resolution image (Thumbnail)
+     * 3 = Single page of multi-page image
+     */
+    public static final int TAG_SUBFILE_TYPE                      = 0x00FF;
+
+    public static final int TAG_IMAGE_WIDTH                       = 0x0100;
+    public static final int TAG_IMAGE_HEIGHT                      = 0x0101;
+
+    /**
+     * When image format is no compression, this value shows the number of bits
+     * per component for each pixel. Usually this value is '8,8,8'.
+     */
+    public static final int TAG_BITS_PER_SAMPLE                   = 0x0102;
+    public static final int TAG_COMPRESSION                       = 0x0103;
+
+    /**
+     * Shows the color space of the image data components.
+     * 0 = WhiteIsZero
+     * 1 = BlackIsZero
+     * 2 = RGB
+     * 3 = RGB Palette
+     * 4 = Transparency Mask
+     * 5 = CMYK
+     * 6 = YCbCr
+     * 8 = CIELab
+     * 9 = ICCLab
+     * 10 = ITULab
+     * 32803 = Color Filter Array
+     * 32844 = Pixar LogL
+     * 32845 = Pixar LogLuv
+     * 34892 = Linear Raw
+     */
+    public static final int TAG_PHOTOMETRIC_INTERPRETATION        = 0x0106;
+
+    /**
+     * 1 = No dithering or halftoning
+     * 2 = Ordered dither or halftone
+     * 3 = Randomized dither
+     */
+    public static final int TAG_THRESHOLDING                      = 0x0107;
+
+    /**
+     * 1 = Normal
+     * 2 = Reversed
+     */
+    public static final int TAG_FILL_ORDER                        = 0x010A;
+    public static final int TAG_DOCUMENT_NAME                     = 0x010D;
+
+    public static final int TAG_IMAGE_DESCRIPTION                 = 0x010E;
+
+    public static final int TAG_MAKE                              = 0x010F;
+    public static final int TAG_MODEL                             = 0x0110;
+    /** The position in the file of raster data. */
+    public static final int TAG_STRIP_OFFSETS                     = 0x0111;
+    public static final int TAG_ORIENTATION                       = 0x0112;
+    /** Each pixel is composed of this many samples. */
+    public static final int TAG_SAMPLES_PER_PIXEL                 = 0x0115;
+    /** The raster is codified by a single block of data holding this many rows. */
+    public static final int TAG_ROWS_PER_STRIP                    = 0x0116;
+    /** The size of the raster data in bytes. */
+    public static final int TAG_STRIP_BYTE_COUNTS                 = 0x0117;
+    public static final int TAG_MIN_SAMPLE_VALUE                  = 0x0118;
+    public static final int TAG_MAX_SAMPLE_VALUE                  = 0x0119;
+    public static final int TAG_X_RESOLUTION                      = 0x011A;
+    public static final int TAG_Y_RESOLUTION                      = 0x011B;
+    /**
+     * When image format is no compression YCbCr, this value shows byte aligns of
+     * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for
+     * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and
+     * stored to Y plane/Cb plane/Cr plane format.
+     */
+    public static final int TAG_PLANAR_CONFIGURATION              = 0x011C;
+    public static final int TAG_PAGE_NAME                         = 0x011D;
+
+    public static final int TAG_RESOLUTION_UNIT                   = 0x0128;
+    public static final int TAG_TRANSFER_FUNCTION                 = 0x012D;
+    public static final int TAG_SOFTWARE                          = 0x0131;
+    public static final int TAG_DATETIME                          = 0x0132;
+    public static final int TAG_ARTIST                            = 0x013B;
+    public static final int TAG_HOST_COMPUTER                     = 0x013C;
+    public static final int TAG_PREDICTOR                         = 0x013D;
+    public static final int TAG_WHITE_POINT                       = 0x013E;
+    public static final int TAG_PRIMARY_CHROMATICITIES            = 0x013F;
+
+    public static final int TAG_TILE_WIDTH                        = 0x0142;
+    public static final int TAG_TILE_LENGTH                       = 0x0143;
+    public static final int TAG_TILE_OFFSETS                      = 0x0144;
+    public static final int TAG_TILE_BYTE_COUNTS                  = 0x0145;
+
+    public static final int TAG_SUB_IFD_OFFSET                    = 0x014a;
+
+    public static final int TAG_TRANSFER_RANGE                    = 0x0156;
+    public static final int TAG_JPEG_TABLES                       = 0x015B;
+    public static final int TAG_JPEG_PROC                         = 0x0200;
+
+    public static final int TAG_YCBCR_COEFFICIENTS                = 0x0211;
+    public static final int TAG_YCBCR_SUBSAMPLING                 = 0x0212;
+    public static final int TAG_YCBCR_POSITIONING                 = 0x0213;
+    public static final int TAG_REFERENCE_BLACK_WHITE             = 0x0214;
+
+    public static final int TAG_RELATED_IMAGE_FILE_FORMAT         = 0x1000;
+    public static final int TAG_RELATED_IMAGE_WIDTH               = 0x1001;
+    public static final int TAG_RELATED_IMAGE_HEIGHT              = 0x1002;
+
+    public static final int TAG_RATING                            = 0x4746;
+
+    public static final int TAG_CFA_REPEAT_PATTERN_DIM            = 0x828D;
+    /** There are two definitions for CFA pattern, I don't know the difference... */
+    public static final int TAG_CFA_PATTERN_2                     = 0x828E;
+    public static final int TAG_BATTERY_LEVEL                     = 0x828F;
+    public static final int TAG_COPYRIGHT                         = 0x8298;
+    /**
+     * Exposure time (reciprocal of shutter speed). Unit is second.
+     */
+    public static final int TAG_EXPOSURE_TIME                     = 0x829A;
+    /**
+     * The actual F-number(F-stop) of lens when the image was taken.
+     */
+    public static final int TAG_FNUMBER                           = 0x829D;
+    public static final int TAG_IPTC_NAA                          = 0x83BB;
+    public static final int TAG_INTER_COLOR_PROFILE               = 0x8773;
+    /**
+     * Exposure program that the camera used when image was taken. '1' means
+     * manual control, '2' program normal, '3' aperture priority, '4' shutter
+     * priority, '5' program creative (slow program), '6' program action
+     * (high-speed program), '7' portrait mode, '8' landscape mode.
+     */
+    public static final int TAG_EXPOSURE_PROGRAM                  = 0x8822;
+    public static final int TAG_SPECTRAL_SENSITIVITY              = 0x8824;
+    public static final int TAG_ISO_EQUIVALENT                    = 0x8827;
+    /**
+     * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.
+     * <p>
+     * OECF is the relationship between the camera optical input and the image values.
+     * <p>
+     * The values are:
+     * <ul>
+     *   <li>Two shorts, indicating respectively number of columns, and number of rows.</li>
+     *   <li>For each column, the column name in a null-terminated ASCII string.</li>
+     *   <li>For each cell, an SRATIONAL value.</li>
+     * </ul>
+     */
+    public static final int TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION = 0x8828;
+    public static final int TAG_INTERLACE                         = 0x8829;
+    public static final int TAG_TIME_ZONE_OFFSET_TIFF_EP          = 0x882A;
+    public static final int TAG_SELF_TIMER_MODE_TIFF_EP           = 0x882B;
+    /**
+     * Applies to ISO tag.
+     *
+     * 0 = Unknown
+     * 1 = Standard Output Sensitivity
+     * 2 = Recommended Exposure Index
+     * 3 = ISO Speed
+     * 4 = Standard Output Sensitivity and Recommended Exposure Index
+     * 5 = Standard Output Sensitivity and ISO Speed
+     * 6 = Recommended Exposure Index and ISO Speed
+     * 7 = Standard Output Sensitivity, Recommended Exposure Index and ISO Speed
+     */
+    public static final int TAG_SENSITIVITY_TYPE                  = 0x8830;
+    public static final int TAG_STANDARD_OUTPUT_SENSITIVITY       = 0x8831;
+    public static final int TAG_RECOMMENDED_EXPOSURE_INDEX        = 0x8832;
+    /** Non-standard, but in use. */
+    public static final int TAG_TIME_ZONE_OFFSET                  = 0x882A;
+    public static final int TAG_SELF_TIMER_MODE                   = 0x882B;
+
+    public static final int TAG_EXIF_VERSION                      = 0x9000;
+    public static final int TAG_DATETIME_ORIGINAL                 = 0x9003;
+    public static final int TAG_DATETIME_DIGITIZED                = 0x9004;
+
+    public static final int TAG_COMPONENTS_CONFIGURATION          = 0x9101;
+    /**
+     * Average (rough estimate) compression level in JPEG bits per pixel.
+     * */
+    public static final int TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL = 0x9102;
+
+    /**
+     * Shutter speed by APEX value. To convert this value to ordinary 'Shutter Speed';
+     * calculate this value's power of 2, then reciprocal. For example, if the
+     * ShutterSpeedValue is '4', shutter speed is 1/(24)=1/16 second.
+     */
+    public static final int TAG_SHUTTER_SPEED                     = 0x9201;
+    /**
+     * The actual aperture value of lens when the image was taken. Unit is APEX.
+     * To convert this value to ordinary F-number (F-stop), calculate this value's
+     * power of root 2 (=1.4142). For example, if the ApertureValue is '5',
+     * F-number is 1.4142^5 = F5.6.
+     */
+    public static final int TAG_APERTURE                          = 0x9202;
+    public static final int TAG_BRIGHTNESS_VALUE                  = 0x9203;
+    public static final int TAG_EXPOSURE_BIAS                     = 0x9204;
+    /**
+     * Maximum aperture value of lens. You can convert to F-number by calculating
+     * power of root 2 (same process of ApertureValue:0x9202).
+     * The actual aperture value of lens when the image was taken. To convert this
+     * value to ordinary f-number(f-stop), calculate the value's power of root 2
+     * (=1.4142). For example, if the ApertureValue is '5', f-number is 1.41425^5 = F5.6.
+     */
+    public static final int TAG_MAX_APERTURE                      = 0x9205;
+    /**
+     * Indicates the distance the autofocus camera is focused to.  Tends to be less accurate as distance increases.
+     */
+    public static final int TAG_SUBJECT_DISTANCE                  = 0x9206;
+    /**
+     * Exposure metering method. '0' means unknown, '1' average, '2' center
+     * weighted average, '3' spot, '4' multi-spot, '5' multi-segment, '6' partial,
+     * '255' other.
+     */
+    public static final int TAG_METERING_MODE                     = 0x9207;
+
+    public static final int TAG_LIGHT_SOURCE                      = 0x9208; // TODO duplicate tag
+    /**
+     * White balance (aka light source). '0' means unknown, '1' daylight,
+     * '2' fluorescent, '3' tungsten, '10' flash, '17' standard light A,
+     * '18' standard light B, '19' standard light C, '20' D55, '21' D65,
+     * '22' D75, '255' other.
+     */
+    public static final int TAG_WHITE_BALANCE                     = 0x9208; // TODO duplicate tag
+    /**
+     * 0x0  = 0000000 = No Flash
+     * 0x1  = 0000001 = Fired
+     * 0x5  = 0000101 = Fired, Return not detected
+     * 0x7  = 0000111 = Fired, Return detected
+     * 0x9  = 0001001 = On
+     * 0xd  = 0001101 = On, Return not detected
+     * 0xf  = 0001111 = On, Return detected
+     * 0x10 = 0010000 = Off
+     * 0x18 = 0011000 = Auto, Did not fire
+     * 0x19 = 0011001 = Auto, Fired
+     * 0x1d = 0011101 = Auto, Fired, Return not detected
+     * 0x1f = 0011111 = Auto, Fired, Return detected
+     * 0x20 = 0100000 = No flash function
+     * 0x41 = 1000001 = Fired, Red-eye reduction
+     * 0x45 = 1000101 = Fired, Red-eye reduction, Return not detected
+     * 0x47 = 1000111 = Fired, Red-eye reduction, Return detected
+     * 0x49 = 1001001 = On, Red-eye reduction
+     * 0x4d = 1001101 = On, Red-eye reduction, Return not detected
+     * 0x4f = 1001111 = On, Red-eye reduction, Return detected
+     * 0x59 = 1011001 = Auto, Fired, Red-eye reduction
+     * 0x5d = 1011101 = Auto, Fired, Red-eye reduction, Return not detected
+     * 0x5f = 1011111 = Auto, Fired, Red-eye reduction, Return detected
+     *        6543210 (positions)
+     *
+     * This is a bitmask.
+     * 0 = flash fired
+     * 1 = return detected
+     * 2 = return able to be detected
+     * 3 = unknown
+     * 4 = auto used
+     * 5 = unknown
+     * 6 = red eye reduction used
+     */
+    public static final int TAG_FLASH                             = 0x9209;
+    /**
+     * Focal length of lens used to take image.  Unit is millimeter.
+     * Nice digital cameras actually save the focal length as a function of how far they are zoomed in.
+     */
+    public static final int TAG_FOCAL_LENGTH                      = 0x920A;
+
+    public static final int TAG_FLASH_ENERGY_TIFF_EP              = 0x920B;
+    public static final int TAG_SPATIAL_FREQ_RESPONSE_TIFF_EP     = 0x920C;
+    public static final int TAG_NOISE                             = 0x920D;
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION_TIFF_EP  = 0x920E;
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION_TIFF_EP = 0x920F;
+    public static final int TAG_IMAGE_NUMBER                      = 0x9211;
+    public static final int TAG_SECURITY_CLASSIFICATION           = 0x9212;
+    public static final int TAG_IMAGE_HISTORY                     = 0x9213;
+    public static final int TAG_SUBJECT_LOCATION_TIFF_EP          = 0x9214;
+    public static final int TAG_EXPOSURE_INDEX_TIFF_EP            = 0x9215;
+    public static final int TAG_STANDARD_ID_TIFF_EP               = 0x9216;
+
+    /**
+     * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs.
+     * To determine the format, we consider the starting bytes of the makernote itself and sometimes the
+     * camera model and make.
+     * <p>
+     * The component count for this tag includes all of the bytes needed for the makernote.
+     */
+    public static final int TAG_MAKERNOTE                         = 0x927C;
+
+    public static final int TAG_USER_COMMENT                      = 0x9286;
+
+    public static final int TAG_SUBSECOND_TIME                    = 0x9290;
+    public static final int TAG_SUBSECOND_TIME_ORIGINAL           = 0x9291;
+    public static final int TAG_SUBSECOND_TIME_DIGITIZED          = 0x9292;
+
+    /** The image title, as used by Windows XP. */
+    public static final int TAG_WIN_TITLE                         = 0x9C9B;
+    /** The image comment, as used by Windows XP. */
+    public static final int TAG_WIN_COMMENT                       = 0x9C9C;
+    /** The image author, as used by Windows XP (called Artist in the Windows shell). */
+    public static final int TAG_WIN_AUTHOR                        = 0x9C9D;
+    /** The image keywords, as used by Windows XP. */
+    public static final int TAG_WIN_KEYWORDS                      = 0x9C9E;
+    /** The image subject, as used by Windows XP. */
+    public static final int TAG_WIN_SUBJECT                       = 0x9C9F;
+
+    public static final int TAG_FLASHPIX_VERSION                  = 0xA000;
+    /**
+     * Defines Color Space. DCF image must use sRGB color space so value is
+     * always '1'. If the picture uses the other color space, value is
+     * '65535':Uncalibrated.
+     */
+    public static final int TAG_COLOR_SPACE                       = 0xA001;
+    public static final int TAG_EXIF_IMAGE_WIDTH                  = 0xA002;
+    public static final int TAG_EXIF_IMAGE_HEIGHT                 = 0xA003;
+    public static final int TAG_RELATED_SOUND_FILE                = 0xA004;
+
+    public static final int TAG_FLASH_ENERGY                      = 0xA20B;
+    public static final int TAG_SPATIAL_FREQ_RESPONSE             = 0xA20C;
+    public static final int TAG_FOCAL_PLANE_X_RESOLUTION          = 0xA20E;
+    public static final int TAG_FOCAL_PLANE_Y_RESOLUTION          = 0xA20F;
+    /**
+     * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
+     * '2' inch, '3' centimeter.
+     *
+     * Note: Some of Fujifilm's digicam(e.g.FX2700,FX2900,Finepix4700Z/40i etc)
+     * uses value '3' so it must be 'centimeter', but it seems that they use a
+     * '8.3mm?'(1/3in.?) to their ResolutionUnit. Fuji's BUG? Finepix4900Z has
+     * been changed to use value '2' but it doesn't match to actual value also.
+     */
+    public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT       = 0xA210;
+    public static final int TAG_SUBJECT_LOCATION                  = 0xA214;
+    public static final int TAG_EXPOSURE_INDEX                    = 0xA215;
+    public static final int TAG_SENSING_METHOD                    = 0xA217;
+
+    public static final int TAG_FILE_SOURCE                       = 0xA300;
+    public static final int TAG_SCENE_TYPE                        = 0xA301;
+    public static final int TAG_CFA_PATTERN                       = 0xA302;
+
+    /**
+     * This tag indicates the use of special processing on image data, such as rendering
+     * geared to output. When special processing is performed, the reader is expected to
+     * disable or minimize any further processing.
+     * Tag = 41985 (A401.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal process
+     *   1 = Custom process
+     *   Other = reserved
+     */
+    public static final int TAG_CUSTOM_RENDERED                   = 0xA401;
+    /**
+     * This tag indicates the exposure mode set when the image was shot. In auto-bracketing
+     * mode, the camera shoots a series of frames of the same scene at different exposure settings.
+     * Tag = 41986 (A402.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = Auto exposure
+     *   1 = Manual exposure
+     *   2 = Auto bracket
+     *   Other = reserved
+     */
+    public static final int TAG_EXPOSURE_MODE                     = 0xA402;
+    /**
+     * This tag indicates the white balance mode set when the image was shot.
+     * Tag = 41987 (A403.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = Auto white balance
+     *   1 = Manual white balance
+     *   Other = reserved
+     */
+    public static final int TAG_WHITE_BALANCE_MODE                = 0xA403;
+    /**
+     * This tag indicates the digital zoom ratio when the image was shot. If the
+     * numerator of the recorded value is 0, this indicates that digital zoom was
+     * not used.
+     * Tag = 41988 (A404.H)
+     * Type = RATIONAL
+     * Count = 1
+     * Default = none
+     */
+    public static final int TAG_DIGITAL_ZOOM_RATIO                = 0xA404;
+    /**
+     * This tag indicates the equivalent focal length assuming a 35mm film camera,
+     * in mm. A value of 0 means the focal length is unknown. Note that this tag
+     * differs from the FocalLength tag.
+     * Tag = 41989 (A405.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     */
+    public static final int TAG_35MM_FILM_EQUIV_FOCAL_LENGTH      = 0xA405;
+    /**
+     * This tag indicates the type of scene that was shot. It can also be used to
+     * record the mode in which the image was shot. Note that this differs from
+     * the scene type (SceneType) tag.
+     * Tag = 41990 (A406.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Standard
+     *   1 = Landscape
+     *   2 = Portrait
+     *   3 = Night scene
+     *   Other = reserved
+     */
+    public static final int TAG_SCENE_CAPTURE_TYPE                = 0xA406;
+    /**
+     * This tag indicates the degree of overall image gain adjustment.
+     * Tag = 41991 (A407.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = None
+     *   1 = Low gain up
+     *   2 = High gain up
+     *   3 = Low gain down
+     *   4 = High gain down
+     *   Other = reserved
+     */
+    public static final int TAG_GAIN_CONTROL                      = 0xA407;
+    /**
+     * This tag indicates the direction of contrast processing applied by the camera
+     * when the image was shot.
+     * Tag = 41992 (A408.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Soft
+     *   2 = Hard
+     *   Other = reserved
+     */
+    public static final int TAG_CONTRAST                          = 0xA408;
+    /**
+     * This tag indicates the direction of saturation processing applied by the camera
+     * when the image was shot.
+     * Tag = 41993 (A409.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Low saturation
+     *   2 = High saturation
+     *   Other = reserved
+     */
+    public static final int TAG_SATURATION                        = 0xA409;
+    /**
+     * This tag indicates the direction of sharpness processing applied by the camera
+     * when the image was shot.
+     * Tag = 41994 (A40A.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Soft
+     *   2 = Hard
+     *   Other = reserved
+     */
+    public static final int TAG_SHARPNESS                         = 0xA40A;
+    /**
+     * This tag indicates information on the picture-taking conditions of a particular
+     * camera model. The tag is used only to indicate the picture-taking conditions in
+     * the reader.
+     * Tag = 41995 (A40B.H)
+     * Type = UNDEFINED
+     * Count = Any
+     * Default = none
+     *
+     * The information is recorded in the format shown below. The data is recorded
+     * in Unicode using SHORT type for the number of display rows and columns and
+     * UNDEFINED type for the camera settings. The Unicode (UCS-2) string including
+     * Signature is NULL terminated. The specifics of the Unicode string are as given
+     * in ISO/IEC 10464-1.
+     *
+     *      Length  Type        Meaning
+     *      ------+-----------+------------------
+     *      2       SHORT       Display columns
+     *      2       SHORT       Display rows
+     *      Any     UNDEFINED   Camera setting-1
+     *      Any     UNDEFINED   Camera setting-2
+     *      :       :           :
+     *      Any     UNDEFINED   Camera setting-n
+     */
+    public static final int TAG_DEVICE_SETTING_DESCRIPTION        = 0xA40B;
+    /**
+     * This tag indicates the distance to the subject.
+     * Tag = 41996 (A40C.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = unknown
+     *   1 = Macro
+     *   2 = Close view
+     *   3 = Distant view
+     *   Other = reserved
+     */
+    public static final int TAG_SUBJECT_DISTANCE_RANGE            = 0xA40C;
+
+    /**
+     * This tag indicates an identifier assigned uniquely to each image. It is
+     * recorded as an ASCII string equivalent to hexadecimal notation and 128-bit
+     * fixed length.
+     * Tag = 42016 (A420.H)
+     * Type = ASCII
+     * Count = 33
+     * Default = none
+     */
+    public static final int TAG_IMAGE_UNIQUE_ID                   = 0xA420;
+    /** String. */
+    public static final int TAG_CAMERA_OWNER_NAME                 = 0xA430;
+    /** String. */
+    public static final int TAG_BODY_SERIAL_NUMBER                = 0xA431;
+    /** An array of four Rational64u numbers giving focal and aperture ranges. */
+    public static final int TAG_LENS_SPECIFICATION                = 0xA432;
+    /** String. */
+    public static final int TAG_LENS_MAKE                         = 0xA433;
+    /** String. */
+    public static final int TAG_LENS_MODEL                        = 0xA434;
+    /** String. */
+    public static final int TAG_LENS_SERIAL_NUMBER                = 0xA435;
+    /** Rational64u. */
+    public static final int TAG_GAMMA                             = 0xA500;
+
+    public static final int TAG_PRINT_IM                          = 0xC4A5;
+
+    public static final int TAG_PANASONIC_TITLE                   = 0xC6D2;
+    public static final int TAG_PANASONIC_TITLE_2                 = 0xC6D3;
+
+    public static final int TAG_PADDING                           = 0xEA1C;
+
+    public static final int TAG_LENS                              = 0xFDEA;
+
+    protected static void addExifTagNames(HashMap<Integer, String> map)
+    {
+        map.put(TAG_INTEROP_INDEX, "Interoperability Index");
+        map.put(TAG_INTEROP_VERSION, "Interoperability Version");
+        map.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type");
+        map.put(TAG_SUBFILE_TYPE, "Subfile Type");
+        map.put(TAG_IMAGE_WIDTH, "Image Width");
+        map.put(TAG_IMAGE_HEIGHT, "Image Height");
+        map.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample");
+        map.put(TAG_COMPRESSION, "Compression");
+        map.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation");
+        map.put(TAG_THRESHOLDING, "Thresholding");
+        map.put(TAG_FILL_ORDER, "Fill Order");
+        map.put(TAG_DOCUMENT_NAME, "Document Name");
+        map.put(TAG_IMAGE_DESCRIPTION, "Image Description");
+        map.put(TAG_MAKE, "Make");
+        map.put(TAG_MODEL, "Model");
+        map.put(TAG_STRIP_OFFSETS, "Strip Offsets");
+        map.put(TAG_ORIENTATION, "Orientation");
+        map.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel");
+        map.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
+        map.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
+        map.put(TAG_MIN_SAMPLE_VALUE, "Minimum sample value");
+        map.put(TAG_MAX_SAMPLE_VALUE, "Maximum sample value");
+        map.put(TAG_X_RESOLUTION, "X Resolution");
+        map.put(TAG_Y_RESOLUTION, "Y Resolution");
+        map.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration");
+        map.put(TAG_PAGE_NAME, "Page Name");
+        map.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
+        map.put(TAG_TRANSFER_FUNCTION, "Transfer Function");
+        map.put(TAG_SOFTWARE, "Software");
+        map.put(TAG_DATETIME, "Date/Time");
+        map.put(TAG_ARTIST, "Artist");
+        map.put(TAG_PREDICTOR, "Predictor");
+        map.put(TAG_HOST_COMPUTER, "Host Computer");
+        map.put(TAG_WHITE_POINT, "White Point");
+        map.put(TAG_PRIMARY_CHROMATICITIES, "Primary Chromaticities");
+        map.put(TAG_TILE_WIDTH, "Tile Width");
+        map.put(TAG_TILE_LENGTH, "Tile Length");
+        map.put(TAG_TILE_OFFSETS, "Tile Offsets");
+        map.put(TAG_TILE_BYTE_COUNTS, "Tile Byte Counts");
+        map.put(TAG_SUB_IFD_OFFSET, "Sub IFD Pointer(s)");
+        map.put(TAG_TRANSFER_RANGE, "Transfer Range");
+        map.put(TAG_JPEG_TABLES, "JPEG Tables");
+        map.put(TAG_JPEG_PROC, "JPEG Proc");
+        map.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
+        map.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
+        map.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
+        map.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+        map.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format");
+        map.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width");
+        map.put(TAG_RELATED_IMAGE_HEIGHT, "Related Image Height");
+        map.put(TAG_RATING, "Rating");
+        map.put(TAG_CFA_REPEAT_PATTERN_DIM, "CFA Repeat Pattern Dim");
+        map.put(TAG_CFA_PATTERN_2, "CFA Pattern");
+        map.put(TAG_BATTERY_LEVEL, "Battery Level");
+        map.put(TAG_COPYRIGHT, "Copyright");
+        map.put(TAG_EXPOSURE_TIME, "Exposure Time");
+        map.put(TAG_FNUMBER, "F-Number");
+        map.put(TAG_IPTC_NAA, "IPTC/NAA");
+        map.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile");
+        map.put(TAG_EXPOSURE_PROGRAM, "Exposure Program");
+        map.put(TAG_SPECTRAL_SENSITIVITY, "Spectral Sensitivity");
+        map.put(TAG_ISO_EQUIVALENT, "ISO Speed Ratings");
+        map.put(TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, "Opto-electric Conversion Function (OECF)");
+        map.put(TAG_INTERLACE, "Interlace");
+        map.put(TAG_TIME_ZONE_OFFSET_TIFF_EP, "Time Zone Offset");
+        map.put(TAG_SELF_TIMER_MODE_TIFF_EP, "Self Timer Mode");
+        map.put(TAG_SENSITIVITY_TYPE, "Sensitivity Type");
+        map.put(TAG_STANDARD_OUTPUT_SENSITIVITY, "Standard Output Sensitivity");
+        map.put(TAG_RECOMMENDED_EXPOSURE_INDEX, "Recommended Exposure Index");
+        map.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
+        map.put(TAG_SELF_TIMER_MODE, "Self Timer Mode");
+        map.put(TAG_EXIF_VERSION, "Exif Version");
+        map.put(TAG_DATETIME_ORIGINAL, "Date/Time Original");
+        map.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized");
+        map.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration");
+        map.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel");
+        map.put(TAG_SHUTTER_SPEED, "Shutter Speed Value");
+        map.put(TAG_APERTURE, "Aperture Value");
+        map.put(TAG_BRIGHTNESS_VALUE, "Brightness Value");
+        map.put(TAG_EXPOSURE_BIAS, "Exposure Bias Value");
+        map.put(TAG_MAX_APERTURE, "Max Aperture Value");
+        map.put(TAG_SUBJECT_DISTANCE, "Subject Distance");
+        map.put(TAG_METERING_MODE, "Metering Mode");
+        map.put(TAG_LIGHT_SOURCE, "Light Source");
+        map.put(TAG_WHITE_BALANCE, "White Balance");
+        map.put(TAG_FLASH, "Flash");
+        map.put(TAG_FOCAL_LENGTH, "Focal Length");
+        map.put(TAG_FLASH_ENERGY_TIFF_EP, "Flash Energy");
+        map.put(TAG_SPATIAL_FREQ_RESPONSE_TIFF_EP, "Spatial Frequency Response");
+        map.put(TAG_NOISE, "Noise");
+        map.put(TAG_FOCAL_PLANE_X_RESOLUTION_TIFF_EP, "Focal Plane X Resolution");
+        map.put(TAG_FOCAL_PLANE_Y_RESOLUTION_TIFF_EP, "Focal Plane Y Resolution");
+        map.put(TAG_IMAGE_NUMBER, "Image Number");
+        map.put(TAG_SECURITY_CLASSIFICATION, "Security Classification");
+        map.put(TAG_IMAGE_HISTORY, "Image History");
+        map.put(TAG_SUBJECT_LOCATION_TIFF_EP, "Subject Location");
+        map.put(TAG_EXPOSURE_INDEX_TIFF_EP, "Exposure Index");
+        map.put(TAG_STANDARD_ID_TIFF_EP, "TIFF/EP Standard ID");
+        map.put(TAG_MAKERNOTE, "Makernote");
+        map.put(TAG_USER_COMMENT, "User Comment");
+        map.put(TAG_SUBSECOND_TIME, "Sub-Sec Time");
+        map.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original");
+        map.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized");
+        map.put(TAG_WIN_TITLE, "Windows XP Title");
+        map.put(TAG_WIN_COMMENT, "Windows XP Comment");
+        map.put(TAG_WIN_AUTHOR, "Windows XP Author");
+        map.put(TAG_WIN_KEYWORDS, "Windows XP Keywords");
+        map.put(TAG_WIN_SUBJECT, "Windows XP Subject");
+        map.put(TAG_FLASHPIX_VERSION, "FlashPix Version");
+        map.put(TAG_COLOR_SPACE, "Color Space");
+        map.put(TAG_EXIF_IMAGE_WIDTH, "Exif Image Width");
+        map.put(TAG_EXIF_IMAGE_HEIGHT, "Exif Image Height");
+        map.put(TAG_RELATED_SOUND_FILE, "Related Sound File");
+        map.put(TAG_FLASH_ENERGY, "Flash Energy");
+        map.put(TAG_SPATIAL_FREQ_RESPONSE, "Spatial Frequency Response");
+        map.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution");
+        map.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution");
+        map.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit");
+        map.put(TAG_SUBJECT_LOCATION, "Subject Location");
+        map.put(TAG_EXPOSURE_INDEX, "Exposure Index");
+        map.put(TAG_SENSING_METHOD, "Sensing Method");
+        map.put(TAG_FILE_SOURCE, "File Source");
+        map.put(TAG_SCENE_TYPE, "Scene Type");
+        map.put(TAG_CFA_PATTERN, "CFA Pattern");
+        map.put(TAG_CUSTOM_RENDERED, "Custom Rendered");
+        map.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        map.put(TAG_WHITE_BALANCE_MODE, "White Balance Mode");
+        map.put(TAG_DIGITAL_ZOOM_RATIO, "Digital Zoom Ratio");
+        map.put(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, "Focal Length 35");
+        map.put(TAG_SCENE_CAPTURE_TYPE, "Scene Capture Type");
+        map.put(TAG_GAIN_CONTROL, "Gain Control");
+        map.put(TAG_CONTRAST, "Contrast");
+        map.put(TAG_SATURATION, "Saturation");
+        map.put(TAG_SHARPNESS, "Sharpness");
+        map.put(TAG_DEVICE_SETTING_DESCRIPTION, "Device Setting Description");
+        map.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range");
+        map.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID");
+        map.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name");
+        map.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number");
+        map.put(TAG_LENS_SPECIFICATION, "Lens Specification");
+        map.put(TAG_LENS_MAKE, "Lens Make");
+        map.put(TAG_LENS_MODEL, "Lens Model");
+        map.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        map.put(TAG_GAMMA, "Gamma");
+        map.put(TAG_PRINT_IM, "Print IM");
+        map.put(TAG_PANASONIC_TITLE, "Panasonic Title");
+        map.put(TAG_PANASONIC_TITLE_2, "Panasonic Title (2)");
+        map.put(TAG_PADDING, "Padding");
+        map.put(TAG_LENS, "Lens");
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 8243)
@@ -22,12 +22,5 @@
 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.*;
 
 /**
@@ -36,175 +29,9 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory>
+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);
-    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 8243)
@@ -23,5 +23,4 @@
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
 
 import java.util.HashMap;
@@ -32,24 +31,6 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifIFD0Directory extends Directory
+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;
@@ -58,19 +39,8 @@
     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
@@ -79,34 +49,5 @@
     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);
     }
 
Index: trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 8243)
@@ -22,8 +22,4 @@
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.TagDescriptor;
-
-import static com.drew.metadata.exif.ExifInteropDirectory.*;
 
 /**
@@ -32,5 +28,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory>
+public class ExifInteropDescriptor extends ExifDescriptorBase<ExifInteropDirectory>
 {
     public ExifInteropDescriptor(@NotNull ExifInteropDirectory directory)
@@ -38,36 +34,3 @@
         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 + ")";
-    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 8243)
@@ -22,5 +22,4 @@
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
 
 import java.util.HashMap;
@@ -31,12 +30,6 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifInteropDirectory extends Directory
+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>();
@@ -44,9 +37,5 @@
     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);
     }
 
Index: trunk/src/com/drew/metadata/exif/ExifReader.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 8243)
@@ -26,4 +26,5 @@
 import com.drew.imaging.tiff.TiffReader;
 import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
 import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
@@ -41,9 +42,6 @@
 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;
@@ -65,45 +63,32 @@
     }
 
-    public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType)
+    public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
     {
-        return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE);
+        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());
+        }
     }
 
-    public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType)
+    /** Reads TIFF formatted Exif data from start of the specified {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
     {
-        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");
+        extract(reader, metadata, 0);
+    }
 
+    /** Reads TIFF formatted Exif data a specified offset within a {@link RandomAccessReader}. */
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset)
+    {
         try {
-            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()
+                readerOffset
             );
-
         } catch (TiffProcessingException e) {
             // TODO what do to with this error state?
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 8243)
@@ -21,16 +21,5 @@
 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.*;
 
 /**
@@ -39,797 +28,9 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory>
+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();
-    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 8243)
@@ -22,5 +22,4 @@
 
 import com.drew.lang.annotations.NotNull;
-import com.drew.metadata.Directory;
 
 import java.util.HashMap;
@@ -31,486 +30,13 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifSubIFDDirectory extends Directory
+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
@@ -519,133 +45,5 @@
     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);
     }
 
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 8243)
@@ -22,8 +22,6 @@
 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.*;
@@ -34,12 +32,6 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirectory>
+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)
     {
@@ -47,20 +39,4 @@
     }
 
-    // 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
@@ -68,134 +44,12 @@
     {
         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";
         }
     }
@@ -241,25 +95,4 @@
 
     @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()
     {
@@ -274,54 +107,3 @@
         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");
-    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 8243)
@@ -24,5 +24,4 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.Directory;
 import com.drew.metadata.MetadataException;
 
@@ -36,14 +35,14 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class ExifThumbnailDirectory extends Directory
+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'.
+    /**
+     * The offset to thumbnail image bytes.
      */
-    public static final int TAG_BITS_PER_SAMPLE = 0x0102;
+    public static final int TAG_THUMBNAIL_OFFSET = 0x0201;
+    /**
+     * The size of the thumbnail image data in bytes.
+     */
+    public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
 
     /**
@@ -79,87 +78,14 @@
     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.
-     */
-    public static final int TAG_THUMBNAIL_OFFSET = 0x0201;
-    /**
-     * The size of the thumbnail image data in bytes.
-     */
-    public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
-    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;
-
     @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");
+    static
+    {
+        addExifTagNames(_tagNameMap);
+
         _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");
         _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");
     }
 
Index: trunk/src/com/drew/metadata/exif/ExifTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/ExifTiffHandler.java	(revision 8243)
@@ -57,7 +57,8 @@
         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) {
+        if (marker != standardTiffMarker && marker != olympusRawTiffMarker && marker != olympusRawTiffMarker2 && marker != panasonicRawTiffMarker) {
             throw new TiffProcessingException("Unexpected TIFF marker: 0x" + Integer.toHexString(marker));
         }
@@ -127,5 +128,5 @@
         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);
+            ExifThumbnailDirectory thumbnailDirectory = _metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
             if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
                 Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
@@ -149,5 +150,5 @@
     {
         // Determine the camera model and makernote format.
-        Directory ifd0Directory = _metadata.getDirectory(ExifIFD0Directory.class);
+        Directory ifd0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
 
         if (ifd0Directory == null)
@@ -219,5 +220,7 @@
         } 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);
Index: trunk/src/com/drew/metadata/exif/GpsDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 8243)
@@ -109,6 +109,12 @@
     {
         // 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.00");
+        return timeComponents == null
+            ? null
+            : String.format("%02d:%02d:%s UTC",
+                timeComponents[0].intValue(),
+                timeComponents[1].intValue(),
+                df.format(timeComponents[2].doubleValue()));
     }
 
Index: trunk/src/com/drew/metadata/exif/GpsDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 8243)
@@ -25,5 +25,4 @@
 import com.drew.lang.annotations.NotNull;
 import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.Directory;
 
 import java.util.HashMap;
@@ -34,5 +33,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class GpsDirectory extends Directory
+public class GpsDirectory extends ExifDirectoryBase
 {
     /** GPS tag version GPSVersionID 0 0 BYTE 4 */
@@ -102,4 +101,6 @@
     static
     {
+        addExifTagNames(_tagNameMap);
+
         _tagNameMap.put(TAG_VERSION_ID, "GPS Version ID");
         _tagNameMap.put(TAG_LATITUDE_REF, "GPS Latitude Ref");
Index: trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java	(revision 8243)
@@ -82,4 +82,6 @@
             case CameraSettings.TAG_EXPOSURE_MODE:
                 return getExposureModeDescription();
+            case CameraSettings.TAG_LENS_TYPE:
+                return getLensTypeDescription();
             case CameraSettings.TAG_LONG_FOCAL_LENGTH:
                 return getLongFocalLengthDescription();
@@ -451,4 +453,13 @@
 
     @Nullable
+    public String getLensTypeDescription() {
+        Integer value = _directory.getInteger(CameraSettings.TAG_LENS_TYPE);
+        if (value == null)
+            return null;
+
+        return "Lens type: " + Integer.toString(value);
+    }
+
+    @Nullable
     public String getAfPointSelectedDescription()
     {
Index: trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java	(revision 8243)
@@ -245,5 +245,5 @@
         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;
@@ -519,5 +519,5 @@
         _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_LENS_TYPE, "Lens Type");
         _tagNameMap.put(CameraSettings.TAG_UNKNOWN_9, "Unknown Camera Setting 9");
         _tagNameMap.put(CameraSettings.TAG_UNKNOWN_10, "Unknown Camera Setting 10");
Index: trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java	(revision 8243)
@@ -51,4 +51,6 @@
     public static final int TAG_MINOLTA_THUMBNAIL_LENGTH = 0x0089;
 
+    public static final int TAG_THUMBNAIL_IMAGE = 0x0100;
+
     /**
      * Used by Konica / Minolta cameras
@@ -83,4 +85,5 @@
     public static final int TAG_IMAGE_QUALITY_2 = 0x0103;
 
+    public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104;
 
     /**
@@ -136,4 +139,13 @@
     public static final int TAG_ORIGINAL_MANUFACTURER_MODEL = 0x020D;
 
+    public static final int TAG_PREVIEW_IMAGE = 0x0280;
+    public static final int TAG_PRE_CAPTURE_FRAMES = 0x0300;
+    public static final int TAG_WHITE_BOARD = 0x0301;
+    public static final int TAG_ONE_TOUCH_WB = 0x0302;
+    public static final int TAG_WHITE_BALANCE_BRACKET = 0x0303;
+    public static final int TAG_WHITE_BALANCE_BIAS = 0x0304;
+    public static final int TAG_SCENE_MODE = 0x0403;
+    public static final int TAG_FIRMWARE = 0x0404;
+
     /**
      * See the PIM specification here:
@@ -142,5 +154,6 @@
     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;
@@ -149,5 +162,9 @@
     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;
@@ -156,11 +173,29 @@
     public static final int TAG_MACRO_FOCUS = 0x100E;
     public static final int TAG_SHARPNESS = 0x100F;
+    public static final int TAG_FLASH_CHARGE_LEVEL = 0x1010;
     public static final int TAG_COLOUR_MATRIX = 0x1011;
     public static final int TAG_BLACK_LEVEL = 0x1012;
+//    public static final int TAG_ = 0x1013;
+//    public static final int TAG_ = 0x1014;
     public static final int TAG_WHITE_BALANCE = 0x1015;
+//    public static final int TAG_ = 0x1016;
     public static final int TAG_RED_BIAS = 0x1017;
     public static final int TAG_BLUE_BIAS = 0x1018;
+    public static final int TAG_COLOR_MATRIX_NUMBER = 0x1019;
     public static final int TAG_SERIAL_NUMBER = 0x101A;
+//    public static final int TAG_ = 0x101B;
+//    public static final int TAG_ = 0x101C;
+//    public static final int TAG_ = 0x101D;
+//    public static final int TAG_ = 0x101E;
+//    public static final int TAG_ = 0x101F;
+//    public static final int TAG_ = 0x1020;
+//    public static final int TAG_ = 0x1021;
+//    public static final int TAG_ = 0x1022;
     public static final int TAG_FLASH_BIAS = 0x1023;
+//    public static final int TAG_ = 0x1024;
+//    public static final int TAG_ = 0x1025;
+    public static final int TAG_EXTERNAL_FLASH_BOUNCE = 0x1026;
+    public static final int TAG_EXTERNAL_FLASH_ZOOM = 0x1027;
+    public static final int TAG_EXTERNAL_FLASH_MODE = 0x1028;
     public static final int TAG_CONTRAST = 0x1029;
     public static final int TAG_SHARPNESS_FACTOR = 0x102A;
@@ -170,5 +205,24 @@
     public static final int TAG_FINAL_WIDTH = 0x102E;
     public static final int TAG_FINAL_HEIGHT = 0x102F;
+//    public static final int TAG_ = 0x1030;
+//    public static final int TAG_ = 0x1031;
+//    public static final int TAG_ = 0x1032;
+//    public static final int TAG_ = 0x1033;
     public static final int TAG_COMPRESSION_RATIO = 0x1034;
+    public static final int TAG_THUMBNAIL = 0x1035;
+    public static final int TAG_THUMBNAIL_OFFSET = 0x1036;
+    public static final int TAG_THUMBNAIL_LENGTH = 0x1037;
+//    public static final int TAG_ = 0x1038;
+    public static final int TAG_CCD_SCAN_MODE = 0x1039;
+    public static final int TAG_NOISE_REDUCTION = 0x103A;
+    public static final int TAG_INFINITY_LENS_STEP = 0x103B;
+    public static final int TAG_NEAR_LENS_STEP = 0x103C;
+    public static final int TAG_EQUIPMENT = 0x2010;
+    public static final int TAG_CAMERA_SETTINGS = 0x2020;
+    public static final int TAG_RAW_DEVELOPMENT = 0x2030;
+    public static final int TAG_RAW_DEVELOPMENT_2 = 0x2031;
+    public static final int TAG_IMAGE_PROCESSING = 0x2040;
+    public static final int TAG_FOCUS_INFO = 0x2050;
+    public static final int TAG_RAW_INFO = 0x3000;
 
     public final static class CameraSettings
@@ -191,5 +245,5 @@
         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;
@@ -200,5 +254,5 @@
         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;
@@ -232,4 +286,16 @@
 
     static {
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS_1, "Camera Settings");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS_2, "Camera Settings");
+        _tagNameMap.put(TAG_COMPRESSED_IMAGE_SIZE, "Compressed Image Size");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_1, "Thumbnail Offset");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_2, "Thumbnail Offset");
+        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_THUMBNAIL_IMAGE, "Thumbnail Image");
+        _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_IMAGE_QUALITY_1, "Image Quality");
+        _tagNameMap.put(TAG_IMAGE_QUALITY_2, "Image Quality");
+        _tagNameMap.put(TAG_BODY_FIRMWARE_VERSION, "Body Firmware Version");
         _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode");
         _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality");
@@ -242,20 +308,18 @@
         _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");
-        _tagNameMap.put(TAG_COMPRESSED_IMAGE_SIZE, "Compressed Image Size");
-        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_1, "Thumbnail Offset");
-        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_2, "Thumbnail Offset");
-        _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_LENGTH, "Thumbnail Length");
-        _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode");
-        _tagNameMap.put(TAG_IMAGE_QUALITY_1, "Image Quality");
-        _tagNameMap.put(TAG_IMAGE_QUALITY_2, "Image Quality");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
         _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
-        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
         _tagNameMap.put(TAG_ORIGINAL_MANUFACTURER_MODEL, "Original Manufacturer Model");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE, "Preview Image");
+        _tagNameMap.put(TAG_PRE_CAPTURE_FRAMES, "Pre Capture Frames");
+        _tagNameMap.put(TAG_WHITE_BOARD, "White Board");
+        _tagNameMap.put(TAG_ONE_TOUCH_WB, "One Touch WB");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BRACKET, "White Balance Bracket");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_FIRMWARE, "Firmware");
         _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
-
+        _tagNameMap.put(TAG_DATA_DUMP_1, "Data Dump");
+        _tagNameMap.put(TAG_DATA_DUMP_2, "Data Dump 2");
         _tagNameMap.put(TAG_SHUTTER_SPEED_VALUE, "Shutter Speed Value");
         _tagNameMap.put(TAG_ISO_VALUE, "ISO Value");
@@ -263,5 +327,9 @@
         _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");
@@ -270,4 +338,5 @@
         _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");
@@ -275,6 +344,10 @@
         _tagNameMap.put(TAG_RED_BIAS, "Red Bias");
         _tagNameMap.put(TAG_BLUE_BIAS, "Blue Bias");
+        _tagNameMap.put(TAG_COLOR_MATRIX_NUMBER, "Color Matrix Number");
         _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
         _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_BOUNCE, "External Flash Bounce");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_ZOOM, "External Flash Zoom");
+        _tagNameMap.put(TAG_EXTERNAL_FLASH_MODE, "External Flash Mode");
         _tagNameMap.put(TAG_CONTRAST, "Contrast");
         _tagNameMap.put(TAG_SHARPNESS_FACTOR, "Sharpness Factor");
@@ -285,4 +358,18 @@
         _tagNameMap.put(TAG_FINAL_HEIGHT, "Final Height");
         _tagNameMap.put(TAG_COMPRESSION_RATIO, "Compression Ratio");
+        _tagNameMap.put(TAG_THUMBNAIL, "Thumbnail");
+        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_CCD_SCAN_MODE, "CCD Scan Mode");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_INFINITY_LENS_STEP, "Infinity Lens Step");
+        _tagNameMap.put(TAG_NEAR_LENS_STEP, "Near Lens Step");
+        _tagNameMap.put(TAG_EQUIPMENT, "Equipment");
+        _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings");
+        _tagNameMap.put(TAG_RAW_DEVELOPMENT, "Raw Development");
+        _tagNameMap.put(TAG_RAW_DEVELOPMENT_2, "Raw Development 2");
+        _tagNameMap.put(TAG_IMAGE_PROCESSING, "Image Processing");
+        _tagNameMap.put(TAG_FOCUS_INFO, "Focus Info");
+        _tagNameMap.put(TAG_RAW_INFO, "Raw Info");
 
         _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
Index: trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 8132)
+++ trunk/src/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java	(revision 8243)
@@ -35,5 +35,6 @@
  * 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
  */
 public class PanasonicMakernoteDirectory extends Directory
Index: trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 8243)
+++ trunk/src/com/drew/metadata/file/FileMetadataDescriptor.java	(revision 8243)
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FileMetadataDescriptor extends TagDescriptor<FileMetadataDirectory>
+{
+    public FileMetadataDescriptor(@NotNull FileMetadataDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case FileMetadataDirectory.TAG_FILE_SIZE:
+                return getFileSizeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getFileSizeDescription()
+    {
+        Long size = _directory.getLongObject(FileMetadataDirectory.TAG_FILE_SIZE);
+
+        if (size == null)
+            return null;
+
+        return Long.toString(size) + " bytes";
+    }
+}
+
Index: trunk/src/com/drew/metadata/file/FileMetadataDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 8243)
+++ trunk/src/com/drew/metadata/file/FileMetadataDirectory.java	(revision 8243)
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2015 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class FileMetadataDirectory extends Directory
+{
+    public static final int TAG_FILE_NAME = 1;
+    public static final int TAG_FILE_SIZE = 2;
+    public static final int TAG_FILE_MODIFIED_DATE = 3;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_FILE_NAME, "File Name");
+        _tagNameMap.put(TAG_FILE_SIZE, "File Size");
+        _tagNameMap.put(TAG_FILE_MODIFIED_DATE, "File Modified Date");
+    }
+
+    public FileMetadataDirectory()
+    {
+        this.setDescriptor(new FileMetadataDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "File";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/file/FileMetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 8243)
+++ trunk/src/com/drew/metadata/file/FileMetadataReader.java	(revision 8243)
@@ -0,0 +1,29 @@
+package com.drew.metadata.file;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Date;
+
+public class FileMetadataReader
+{
+    public void read(@NotNull File file, @NotNull Metadata metadata) throws IOException
+    {
+        if (!file.isFile())
+            throw new IOException("File object must reference a file");
+        if (!file.exists())
+            throw new IOException("File does not exist");
+        if (!file.canRead())
+            throw new IOException("File is not readable");
+
+        FileMetadataDirectory directory = new FileMetadataDirectory();
+
+        directory.setString(FileMetadataDirectory.TAG_FILE_NAME, file.getName());
+        directory.setLong(FileMetadataDirectory.TAG_FILE_SIZE, file.length());
+        directory.setDate(FileMetadataDirectory.TAG_FILE_MODIFIED_DATE, new Date(file.lastModified()));
+
+        metadata.addDirectory(directory);
+    }
+}
Index: trunk/src/com/drew/metadata/file/package.html
===================================================================
--- trunk/src/com/drew/metadata/file/package.html	(revision 8243)
+++ trunk/src/com/drew/metadata/file/package.html	(revision 8243)
@@ -0,0 +1,34 @@
+<!--
+  ~ Copyright 2002-2015 Drew Noakes
+  ~
+  ~    Licensed under the Apache License, Version 2.0 (the "License");
+  ~    you may not use this file except in compliance with the License.
+  ~    You may obtain a copy of the License at
+  ~
+  ~        http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~    Unless required by applicable law or agreed to in writing, software
+  ~    distributed under the License is distributed on an "AS IS" BASIS,
+  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~    See the License for the specific language governing permissions and
+  ~    limitations under the License.
+  ~
+  ~ More information about this project is available at:
+  ~
+  ~    https://drewnoakes.com/code/exif/
+  ~    https://github.com/drewnoakes/metadata-extractor
+  -->
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+<body bgcolor="white">
+
+Contains classes for the extraction and modelling of file system metadata.
+
+<!-- Put @see and @since tags down here. -->
+@since 2.8.0
+
+</body>
+</html>
Index: trunk/src/com/drew/metadata/iptc/IptcDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 8132)
+++ trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 8243)
@@ -49,4 +49,8 @@
             case IptcDirectory.TAG_KEYWORDS:
                 return getKeywordsDescription();
+            case IptcDirectory.TAG_TIME_CREATED:
+                return getTimeCreatedDescription();
+            case IptcDirectory.TAG_DIGITAL_TIME_CREATED:
+                return getDigitalTimeCreatedDescription();
             default:
                 return super.getDescription(tagType);
@@ -227,5 +231,21 @@
     public String getTimeCreatedDescription()
     {
-        return _directory.getString(IptcDirectory.TAG_TIME_CREATED);
+        String s = _directory.getString(IptcDirectory.TAG_TIME_CREATED);
+        if (s == null)
+            return null;
+        if (s.length() == 6 || s.length() == 11)
+            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
+        return s;
+    }
+
+    @Nullable
+    public String getDigitalTimeCreatedDescription()
+    {
+        String s = _directory.getString(IptcDirectory.TAG_DIGITAL_TIME_CREATED);
+        if (s == null)
+            return null;
+        if (s.length() == 6 || s.length() == 11)
+            return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4);
+        return s;
     }
 
Index: trunk/src/com/drew/metadata/iptc/IptcReader.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 8132)
+++ trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 8243)
@@ -63,13 +63,12 @@
     }
 
-    public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType)
-    {
-        // Check whether the first byte resembles
-        return segmentBytes.length != 0 && segmentBytes[0] == 0x1c;
-    }
-
-    public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
-    {
-        extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the IPTC marker byte
+            if (segmentBytes.length != 0 && segmentBytes[0] == 0x1c) {
+                extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length);
+            }
+        }
     }
 
@@ -79,5 +78,6 @@
     public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length)
     {
-        IptcDirectory directory = metadata.getOrCreateDirectory(IptcDirectory.class);
+        IptcDirectory directory = new IptcDirectory();
+        metadata.addDirectory(directory);
 
         int offset = 0;
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 8132)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 8243)
@@ -48,10 +48,13 @@
     }
 
-    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)
     {
-        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.setString(JpegCommentDirectory.TAG_COMMENT, new String(segmentBytes));
+        }
     }
 }
Index: trunk/src/com/drew/metadata/jpeg/JpegReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 8132)
+++ trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 8243)
@@ -63,18 +63,15 @@
     }
 
-    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
@@ -101,5 +98,4 @@
                 directory.setObject(JpegDirectory.TAG_COMPONENT_DATA_1 + i, component);
             }
-
         } catch (IOException ex) {
             directory.addError(ex.getMessage());
Index: trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 8132)
+++ trunk/src/com/drew/metadata/tiff/DirectoryTiffHandler.java	(revision 8243)
@@ -41,8 +41,15 @@
     protected final Metadata _metadata;
 
-    protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectory)
+    protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectoryClass)
     {
         _metadata = metadata;
-        _currentDirectory = _metadata.getOrCreateDirectory(initialDirectory);
+        try {
+            _currentDirectory = initialDirectoryClass.newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        _metadata.addDirectory(_currentDirectory);
     }
 
@@ -54,7 +61,13 @@
     protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass)
     {
-        assert(directoryClass != _currentDirectory.getClass());
         _directoryStack.push(_currentDirectory);
-        _currentDirectory = _metadata.getOrCreateDirectory(directoryClass);
+        try {
+            _currentDirectory = directoryClass.newInstance();
+        } catch (InstantiationException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        _metadata.addDirectory(_currentDirectory);
     }
 
