Index: trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 15217)
+++ trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 15218)
@@ -36,5 +36,5 @@
 import com.drew.metadata.exif.ExifReader;
 import com.drew.metadata.file.FileSystemMetadataReader;
-//import com.drew.metadata.icc.IccReader;
+import com.drew.metadata.icc.IccReader;
 import com.drew.metadata.iptc.IptcReader;
 //import com.drew.metadata.jfif.JfifReader;
@@ -44,6 +44,6 @@
 import com.drew.metadata.jpeg.JpegDnlReader;
 import com.drew.metadata.jpeg.JpegReader;
-//import com.drew.metadata.photoshop.DuckyReader;
-//import com.drew.metadata.photoshop.PhotoshopReader;
+import com.drew.metadata.photoshop.DuckyReader;
+import com.drew.metadata.photoshop.PhotoshopReader;
 //import com.drew.metadata.xmp.XmpReader;
 
@@ -62,7 +62,7 @@
             new ExifReader(),
             //new XmpReader(),
-            //new IccReader(),
-            //new PhotoshopReader(),
-            //new DuckyReader(),
+            new IccReader(),
+            new PhotoshopReader(),
+            new DuckyReader(),
             new IptcReader(),
             //new AdobeJpegReader(),
Index: trunk/src/com/drew/metadata/MetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/MetadataReader.java	(revision 15218)
+++ trunk/src/com/drew/metadata/MetadataReader.java	(revision 15218)
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata;
+
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+
+/**
+ * Defines an object capable of processing a particular type of metadata from a {@link RandomAccessReader}.
+ * <p>
+ * Instances of this interface must be thread-safe and reusable.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public interface MetadataReader
+{
+    /**
+     * Extracts metadata from <code>reader</code> and merges it into the specified {@link Metadata} object.
+     *
+     * @param reader   The {@link RandomAccessReader} from which the metadata should be extracted.
+     * @param metadata The {@link Metadata} object into which extracted values should be merged.
+     */
+    void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata);
+}
Index: trunk/src/com/drew/metadata/icc/IccDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/icc/IccDescriptor.java	(revision 15218)
+++ trunk/src/com/drew/metadata/icc/IccDescriptor.java	(revision 15218)
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.icc;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.text.DecimalFormat;
+
+import static com.drew.metadata.icc.IccDirectory.*;
+
+/**
+ * @author Yuri Binev
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class IccDescriptor extends TagDescriptor<IccDirectory>
+{
+    public IccDescriptor(@NotNull IccDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_PROFILE_VERSION:
+                return getProfileVersionDescription();
+            case TAG_PROFILE_CLASS:
+                return getProfileClassDescription();
+            case TAG_PLATFORM:
+                return getPlatformDescription();
+            case TAG_RENDERING_INTENT:
+                return getRenderingIntentDescription();
+        }
+
+        if (tagType > 0x20202020 && tagType < 0x7a7a7a7a)
+            return getTagDataString(tagType);
+
+        return super.getDescription(tagType);
+    }
+
+    private static final int ICC_TAG_TYPE_TEXT = 0x74657874;
+    private static final int ICC_TAG_TYPE_DESC = 0x64657363;
+    private static final int ICC_TAG_TYPE_SIG = 0x73696720;
+    private static final int ICC_TAG_TYPE_MEAS = 0x6D656173;
+    private static final int ICC_TAG_TYPE_XYZ_ARRAY = 0x58595A20;
+    private static final int ICC_TAG_TYPE_MLUC = 0x6d6c7563;
+    private static final int ICC_TAG_TYPE_CURV = 0x63757276;
+
+    @Nullable
+    private String getTagDataString(int tagType)
+    {
+        try {
+            byte[] bytes = _directory.getByteArray(tagType);
+            if (bytes == null)
+                return _directory.getString(tagType);
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            int iccTagType = reader.getInt32(0);
+            switch (iccTagType) {
+                case ICC_TAG_TYPE_TEXT:
+                    try {
+                        return new String(bytes, 8, bytes.length - 8 - 1, "ASCII");
+                    } catch (UnsupportedEncodingException ex) {
+                        return new String(bytes, 8, bytes.length - 8 - 1);
+                    }
+                case ICC_TAG_TYPE_DESC:
+                    int stringLength = reader.getInt32(8);
+                    return new String(bytes, 12, stringLength - 1);
+                case ICC_TAG_TYPE_SIG:
+                    return IccReader.getStringFromInt32(reader.getInt32(8));
+                case ICC_TAG_TYPE_MEAS: {
+                    int observerType = reader.getInt32(8);
+                    float x = reader.getS15Fixed16(12);
+                    float y = reader.getS15Fixed16(16);
+                    float z = reader.getS15Fixed16(20);
+                    int geometryType = reader.getInt32(24);
+                    float flare = reader.getS15Fixed16(28);
+                    int illuminantType = reader.getInt32(32);
+                    String observerString;
+                    switch (observerType) {
+                        case 0:
+                            observerString = "Unknown";
+                            break;
+                        case 1:
+                            observerString = "1931 2\u00B0";
+                            break;
+                        case 2:
+                            observerString = "1964 10\u00B0";
+                            break;
+                        default:
+                            observerString = String.format("Unknown %d", observerType);
+                    }
+                    String geometryString;
+                    switch (geometryType) {
+                        case 0:
+                            geometryString = "Unknown";
+                            break;
+                        case 1:
+                            geometryString = "0/45 or 45/0";
+                            break;
+                        case 2:
+                            geometryString = "0/d or d/0";
+                            break;
+                        default:
+                            geometryString = String.format("Unknown %d", observerType);
+                    }
+                    String illuminantString;
+                    switch (illuminantType) {
+                        case 0:
+                            illuminantString = "unknown";
+                            break;
+                        case 1:
+                            illuminantString = "D50";
+                            break;
+                        case 2:
+                            illuminantString = "D65";
+                            break;
+                        case 3:
+                            illuminantString = "D93";
+                            break;
+                        case 4:
+                            illuminantString = "F2";
+                            break;
+                        case 5:
+                            illuminantString = "D55";
+                            break;
+                        case 6:
+                            illuminantString = "A";
+                            break;
+                        case 7:
+                            illuminantString = "Equi-Power (E)";
+                            break;
+                        case 8:
+                            illuminantString = "F8";
+                            break;
+                        default:
+                            illuminantString = String.format("Unknown %d", illuminantType);
+                            break;
+                    }
+                    DecimalFormat format = new DecimalFormat("0.###");
+                    return String.format("%s Observer, Backing (%s, %s, %s), Geometry %s, Flare %d%%, Illuminant %s",
+                            observerString, format.format(x), format.format(y), format.format(z), geometryString, Math.round(flare * 100), illuminantString);
+                }
+                case ICC_TAG_TYPE_XYZ_ARRAY: {
+                    StringBuilder res = new StringBuilder();
+                    DecimalFormat format = new DecimalFormat("0.####");
+                    int count = (bytes.length - 8) / 12;
+                    for (int i = 0; i < count; i++) {
+                        float x = reader.getS15Fixed16(8 + i * 12);
+                        float y = reader.getS15Fixed16(8 + i * 12 + 4);
+                        float z = reader.getS15Fixed16(8 + i * 12 + 8);
+                        if (i > 0)
+                            res.append(", ");
+                        res.append("(").append(format.format(x)).append(", ").append(format.format(y)).append(", ").append(format.format(z)).append(")");
+                    }
+                    return res.toString();
+                }
+                case ICC_TAG_TYPE_MLUC: {
+                    int int1 = reader.getInt32(8);
+                    StringBuilder res = new StringBuilder();
+                    res.append(int1);
+                    //int int2 = reader.getInt32(12);
+                    //System.err.format("int1: %d, int2: %d\n", int1, int2);
+                    for (int i = 0; i < int1; i++) {
+                        String str = IccReader.getStringFromInt32(reader.getInt32(16 + i * 12));
+                        int len = reader.getInt32(16 + i * 12 + 4);
+                        int ofs = reader.getInt32(16 + i * 12 + 8);
+                        String name;
+                        try {
+                            name = new String(bytes, ofs, len, "UTF-16BE");
+                        } catch (UnsupportedEncodingException ex) {
+                            name = new String(bytes, ofs, len);
+                        }
+                        res.append(" ").append(str).append("(").append(name).append(")");
+                        //System.err.format("% 3d: %s, len: %d, ofs: %d, \"%s\"\n", i, str, len,ofs,name);
+                    }
+                    return res.toString();
+                }
+                case ICC_TAG_TYPE_CURV: {
+                    int num = reader.getInt32(8);
+                    StringBuilder res = new StringBuilder();
+                    for (int i = 0; i < num; i++) {
+                        if (i != 0)
+                            res.append(", ");
+                        res.append(formatDoubleAsString(((float)reader.getUInt16(12 + i * 2)) / 65535.0, 7, false));
+                        //res+=String.format("%1.7g",Math.round(((float)iccReader.getInt16(b,12+i*2))/0.065535)/1E7);
+                    }
+                    return res.toString();
+                }
+                default:
+                    return String.format("%s (0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length);
+            }
+        } catch (IOException e) {
+            // TODO decode these values during IccReader.extract so we can report any errors at that time
+            // It is convention to return null if a description cannot be formulated.
+            // If an error is to be reported, it should be done during the extraction process.
+            return null;
+        }
+    }
+
+    @NotNull
+    public static String formatDoubleAsString(double value, int precision, boolean zeroes)
+    {
+        if (precision < 1)
+            return "" + Math.round(value);
+        long intPart = Math.abs((long)value);
+        long rest = (int)Math.round((Math.abs(value) - intPart) * Math.pow(10, precision));
+        long restKept = rest;
+        String res = "";
+        byte cour;
+        for (int i = precision; i > 0; i--) {
+            cour = (byte)(Math.abs(rest % 10));
+            rest /= 10;
+            if (res.length() > 0 || zeroes || cour != 0 || i == 1)
+                res = cour + res;
+        }
+        intPart += rest;
+        boolean isNegative = ((value < 0) && (intPart != 0 || restKept != 0));
+        return (isNegative ? "-" : "") + intPart + "." + res;
+    }
+
+    @Nullable
+    private String getRenderingIntentDescription()
+    {
+        return getIndexedDescription(TAG_RENDERING_INTENT,
+            "Perceptual",
+            "Media-Relative Colorimetric",
+            "Saturation",
+            "ICC-Absolute Colorimetric");
+    }
+
+    @Nullable
+    private String getPlatformDescription()
+    {
+        String str = _directory.getString(TAG_PLATFORM);
+        if (str==null)
+            return null;
+        // Because Java doesn't allow switching on string values, create an integer from the first four chars
+        // and switch on that instead.
+        int i;
+        try {
+            i = getInt32FromString(str);
+        } catch (IOException e) {
+            return str;
+        }
+        switch (i) {
+            case 0x4150504C: // "APPL"
+                return "Apple Computer, Inc.";
+            case 0x4D534654: // "MSFT"
+                return "Microsoft Corporation";
+            case 0x53474920:
+                return "Silicon Graphics, Inc.";
+            case 0x53554E57:
+                return "Sun Microsystems, Inc.";
+            case 0x54474E54:
+                return "Taligent, Inc.";
+            default:
+                return String.format("Unknown (%s)", str);
+        }
+    }
+
+    @Nullable
+    private String getProfileClassDescription()
+    {
+        String str = _directory.getString(TAG_PROFILE_CLASS);
+        if (str==null)
+            return null;
+        // Because Java doesn't allow switching on string values, create an integer from the first four chars
+        // and switch on that instead.
+        int i;
+        try {
+            i = getInt32FromString(str);
+        } catch (IOException e) {
+            return str;
+        }
+        switch (i) {
+            case 0x73636E72:
+                return "Input Device";
+            case 0x6D6E7472: // mntr
+                return "Display Device";
+            case 0x70727472:
+                return "Output Device";
+            case 0x6C696E6B:
+                return "DeviceLink";
+            case 0x73706163:
+                return "ColorSpace Conversion";
+            case 0x61627374:
+                return "Abstract";
+            case 0x6E6D636C:
+                return "Named Color";
+            default:
+                return String.format("Unknown (%s)", str);
+        }
+    }
+
+    @Nullable
+    private String getProfileVersionDescription()
+    {
+        Integer value = _directory.getInteger(TAG_PROFILE_VERSION);
+
+        if (value == null)
+            return null;
+
+        int m = (value & 0xFF000000) >> 24;
+        int r = (value & 0x00F00000) >> 20;
+        int R = (value & 0x000F0000) >> 16;
+
+        return String.format("%d.%d.%d", m, r, R);
+    }
+
+    private static int getInt32FromString(@NotNull String string) throws IOException
+    {
+        byte[] bytes = string.getBytes();
+        return new ByteArrayReader(bytes).getInt32(0);
+    }
+}
Index: trunk/src/com/drew/metadata/icc/IccDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/icc/IccDirectory.java	(revision 15218)
+++ trunk/src/com/drew/metadata/icc/IccDirectory.java	(revision 15218)
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.icc;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * @author Yuri Binev
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class IccDirectory extends Directory
+{
+    // These (smaller valued) tags have an integer value that's equal to their offset within the ICC data buffer.
+
+    public static final int TAG_PROFILE_BYTE_COUNT = 0;
+    public static final int TAG_CMM_TYPE = 4;
+    public static final int TAG_PROFILE_VERSION = 8;
+    public static final int TAG_PROFILE_CLASS = 12;
+    public static final int TAG_COLOR_SPACE = 16;
+    public static final int TAG_PROFILE_CONNECTION_SPACE = 20;
+    public static final int TAG_PROFILE_DATETIME = 24;
+    public static final int TAG_SIGNATURE = 36;
+    public static final int TAG_PLATFORM = 40;
+    public static final int TAG_CMM_FLAGS = 44;
+    public static final int TAG_DEVICE_MAKE = 48;
+    public static final int TAG_DEVICE_MODEL = 52;
+    public static final int TAG_DEVICE_ATTR = 56;
+    public static final int TAG_RENDERING_INTENT = 64;
+    public static final int TAG_XYZ_VALUES = 68;
+    public static final int TAG_PROFILE_CREATOR = 80;
+    public static final int TAG_TAG_COUNT = 128;
+
+    // These tag values
+
+    public static final int TAG_TAG_A2B0 = 0x41324230;
+    public static final int TAG_TAG_A2B1 = 0x41324231;
+    public static final int TAG_TAG_A2B2 = 0x41324232;
+    public static final int TAG_TAG_bXYZ = 0x6258595A;
+    public static final int TAG_TAG_bTRC = 0x62545243;
+    public static final int TAG_TAG_B2A0 = 0x42324130;
+    public static final int TAG_TAG_B2A1 = 0x42324131;
+    public static final int TAG_TAG_B2A2 = 0x42324132;
+    public static final int TAG_TAG_calt = 0x63616C74;
+    public static final int TAG_TAG_targ = 0x74617267;
+    public static final int TAG_TAG_chad = 0x63686164;
+    public static final int TAG_TAG_chrm = 0x6368726D;
+    public static final int TAG_TAG_cprt = 0x63707274;
+    public static final int TAG_TAG_crdi = 0x63726469;
+    public static final int TAG_TAG_dmnd = 0x646D6E64;
+    public static final int TAG_TAG_dmdd = 0x646D6464;
+    public static final int TAG_TAG_devs = 0x64657673;
+    public static final int TAG_TAG_gamt = 0x67616D74;
+    public static final int TAG_TAG_kTRC = 0x6B545243;
+    public static final int TAG_TAG_gXYZ = 0x6758595A;
+    public static final int TAG_TAG_gTRC = 0x67545243;
+    public static final int TAG_TAG_lumi = 0x6C756D69;
+    public static final int TAG_TAG_meas = 0x6D656173;
+    public static final int TAG_TAG_bkpt = 0x626B7074;
+    public static final int TAG_TAG_wtpt = 0x77747074;
+    public static final int TAG_TAG_ncol = 0x6E636F6C;
+    public static final int TAG_TAG_ncl2 = 0x6E636C32;
+    public static final int TAG_TAG_resp = 0x72657370;
+    public static final int TAG_TAG_pre0 = 0x70726530;
+    public static final int TAG_TAG_pre1 = 0x70726531;
+    public static final int TAG_TAG_pre2 = 0x70726532;
+    public static final int TAG_TAG_desc = 0x64657363;
+    public static final int TAG_TAG_pseq = 0x70736571;
+    public static final int TAG_TAG_psd0 = 0x70736430;
+    public static final int TAG_TAG_psd1 = 0x70736431;
+    public static final int TAG_TAG_psd2 = 0x70736432;
+    public static final int TAG_TAG_psd3 = 0x70736433;
+    public static final int TAG_TAG_ps2s = 0x70733273;
+    public static final int TAG_TAG_ps2i = 0x70733269;
+    public static final int TAG_TAG_rXYZ = 0x7258595A;
+    public static final int TAG_TAG_rTRC = 0x72545243;
+    public static final int TAG_TAG_scrd = 0x73637264;
+    public static final int TAG_TAG_scrn = 0x7363726E;
+    public static final int TAG_TAG_tech = 0x74656368;
+    public static final int TAG_TAG_bfd = 0x62666420;
+    public static final int TAG_TAG_vued = 0x76756564;
+    public static final int TAG_TAG_view = 0x76696577;
+
+    public static final int TAG_TAG_aabg = 0x61616267;
+    public static final int TAG_TAG_aagg = 0x61616767;
+    public static final int TAG_TAG_aarg = 0x61617267;
+    public static final int TAG_TAG_mmod = 0x6D6D6F64;
+    public static final int TAG_TAG_ndin = 0x6E64696E;
+    public static final int TAG_TAG_vcgt = 0x76636774;
+    public static final int TAG_APPLE_MULTI_LANGUAGE_PROFILE_NAME = 0x6473636d;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_PROFILE_BYTE_COUNT, "Profile Size");
+        _tagNameMap.put(TAG_CMM_TYPE, "CMM Type");
+        _tagNameMap.put(TAG_PROFILE_VERSION, "Version");
+        _tagNameMap.put(TAG_PROFILE_CLASS, "Class");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color space");
+        _tagNameMap.put(TAG_PROFILE_CONNECTION_SPACE, "Profile Connection Space");
+        _tagNameMap.put(TAG_PROFILE_DATETIME, "Profile Date/Time");
+        _tagNameMap.put(TAG_SIGNATURE, "Signature");
+        _tagNameMap.put(TAG_PLATFORM, "Primary Platform");
+        _tagNameMap.put(TAG_CMM_FLAGS, "CMM Flags");
+        _tagNameMap.put(TAG_DEVICE_MAKE, "Device manufacturer");
+        _tagNameMap.put(TAG_DEVICE_MODEL, "Device model");
+        _tagNameMap.put(TAG_DEVICE_ATTR, "Device attributes");
+        _tagNameMap.put(TAG_RENDERING_INTENT, "Rendering Intent");
+        _tagNameMap.put(TAG_XYZ_VALUES, "XYZ values");
+        _tagNameMap.put(TAG_PROFILE_CREATOR, "Profile Creator");
+        _tagNameMap.put(TAG_TAG_COUNT, "Tag Count");
+        _tagNameMap.put(TAG_TAG_A2B0, "AToB 0");
+        _tagNameMap.put(TAG_TAG_A2B1, "AToB 1");
+        _tagNameMap.put(TAG_TAG_A2B2, "AToB 2");
+        _tagNameMap.put(TAG_TAG_bXYZ, "Blue Colorant");
+        _tagNameMap.put(TAG_TAG_bTRC, "Blue TRC");
+        _tagNameMap.put(TAG_TAG_B2A0, "BToA 0");
+        _tagNameMap.put(TAG_TAG_B2A1, "BToA 1");
+        _tagNameMap.put(TAG_TAG_B2A2, "BToA 2");
+        _tagNameMap.put(TAG_TAG_calt, "Calibration Date/Time");
+        _tagNameMap.put(TAG_TAG_targ, "Char Target");
+        _tagNameMap.put(TAG_TAG_chad, "Chromatic Adaptation");
+        _tagNameMap.put(TAG_TAG_chrm, "Chromaticity");
+        _tagNameMap.put(TAG_TAG_cprt, "Copyright");
+        _tagNameMap.put(TAG_TAG_crdi, "CrdInfo");
+        _tagNameMap.put(TAG_TAG_dmnd, "Device Mfg Description");
+        _tagNameMap.put(TAG_TAG_dmdd, "Device Model Description");
+        _tagNameMap.put(TAG_TAG_devs, "Device Settings");
+        _tagNameMap.put(TAG_TAG_gamt, "Gamut");
+        _tagNameMap.put(TAG_TAG_kTRC, "Gray TRC");
+        _tagNameMap.put(TAG_TAG_gXYZ, "Green Colorant");
+        _tagNameMap.put(TAG_TAG_gTRC, "Green TRC");
+        _tagNameMap.put(TAG_TAG_lumi, "Luminance");
+        _tagNameMap.put(TAG_TAG_meas, "Measurement");
+        _tagNameMap.put(TAG_TAG_bkpt, "Media Black Point");
+        _tagNameMap.put(TAG_TAG_wtpt, "Media White Point");
+        _tagNameMap.put(TAG_TAG_ncol, "Named Color");
+        _tagNameMap.put(TAG_TAG_ncl2, "Named Color 2");
+        _tagNameMap.put(TAG_TAG_resp, "Output Response");
+        _tagNameMap.put(TAG_TAG_pre0, "Preview 0");
+        _tagNameMap.put(TAG_TAG_pre1, "Preview 1");
+        _tagNameMap.put(TAG_TAG_pre2, "Preview 2");
+        _tagNameMap.put(TAG_TAG_desc, "Profile Description");
+        _tagNameMap.put(TAG_TAG_pseq, "Profile Sequence Description");
+        _tagNameMap.put(TAG_TAG_psd0, "Ps2 CRD 0");
+        _tagNameMap.put(TAG_TAG_psd1, "Ps2 CRD 1");
+        _tagNameMap.put(TAG_TAG_psd2, "Ps2 CRD 2");
+        _tagNameMap.put(TAG_TAG_psd3, "Ps2 CRD 3");
+        _tagNameMap.put(TAG_TAG_ps2s, "Ps2 CSA");
+        _tagNameMap.put(TAG_TAG_ps2i, "Ps2 Rendering Intent");
+        _tagNameMap.put(TAG_TAG_rXYZ, "Red Colorant");
+        _tagNameMap.put(TAG_TAG_rTRC, "Red TRC");
+        _tagNameMap.put(TAG_TAG_scrd, "Screening Desc");
+        _tagNameMap.put(TAG_TAG_scrn, "Screening");
+        _tagNameMap.put(TAG_TAG_tech, "Technology");
+        _tagNameMap.put(TAG_TAG_bfd, "Ucrbg");
+        _tagNameMap.put(TAG_TAG_vued, "Viewing Conditions Description");
+        _tagNameMap.put(TAG_TAG_view, "Viewing Conditions");
+        _tagNameMap.put(TAG_TAG_aabg, "Blue Parametric TRC");
+        _tagNameMap.put(TAG_TAG_aagg, "Green Parametric TRC");
+        _tagNameMap.put(TAG_TAG_aarg, "Red Parametric TRC");
+        _tagNameMap.put(TAG_TAG_mmod, "Make And Model");
+        _tagNameMap.put(TAG_TAG_ndin, "Native Display Information");
+        _tagNameMap.put(TAG_TAG_vcgt, "Video Card Gamma");
+        _tagNameMap.put(TAG_APPLE_MULTI_LANGUAGE_PROFILE_NAME, "Apple Multi-language Profile Name");
+    }
+
+    public IccDirectory()
+    {
+        this.setDescriptor(new IccDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "ICC Profile";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/icc/IccReader.java
===================================================================
--- trunk/src/com/drew/metadata/icc/IccReader.java	(revision 15218)
+++ trunk/src/com/drew/metadata/icc/IccReader.java	(revision 15218)
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.icc;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.DateUtil;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.MetadataReader;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Reads an ICC profile.
+ * <p>
+ * More information about ICC:
+ * <ul>
+ * <li>http://en.wikipedia.org/wiki/ICC_profile</li>
+ * <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ICC_Profile.html</li>
+ * <li>https://developer.apple.com/library/mac/samplecode/ImageApp/Listings/ICC_h.html</li>
+ * </ul>
+ *
+ * @author Yuri Binev
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class IccReader implements JpegSegmentMetadataReader, MetadataReader
+{
+    public static final String JPEG_SEGMENT_PREAMBLE = "ICC_PROFILE";
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.APP2);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
+
+        // ICC data can be spread across multiple JPEG segments.
+        // We concat them together in this buffer for later processing.
+        byte[] buffer = null;
+
+        for (byte[] segmentBytes : segments) {
+            // Skip any segments that do not contain the required preamble
+            if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength)))
+                continue;
+
+            // NOTE we ignore three bytes here -- are they useful for anything?
+
+            // Grow the buffer
+            if (buffer == null) {
+                buffer = new byte[segmentBytes.length - 14];
+                // skip the first 14 bytes
+                System.arraycopy(segmentBytes, 14, buffer, 0, segmentBytes.length - 14);
+            } else {
+                byte[] newBuffer = new byte[buffer.length + segmentBytes.length - 14];
+                System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
+                System.arraycopy(segmentBytes, 14, newBuffer, buffer.length, segmentBytes.length - 14);
+                buffer = newBuffer;
+            }
+        }
+
+        if (buffer != null)
+            extract(new ByteArrayReader(buffer), metadata);
+    }
+
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
+    {
+        extract(reader, metadata, null);
+    }
+
+    public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, @Nullable Directory parentDirectory)
+    {
+        // TODO review whether the 'tagPtr' values below really do require RandomAccessReader or whether SequentialReader may be used instead
+
+        IccDirectory directory = new IccDirectory();
+
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
+
+        try {
+            int profileByteCount = reader.getInt32(IccDirectory.TAG_PROFILE_BYTE_COUNT);
+            directory.setInt(IccDirectory.TAG_PROFILE_BYTE_COUNT, profileByteCount);
+
+            // For these tags, the int value of the tag is in fact it's offset within the buffer.
+            set4ByteString(directory, IccDirectory.TAG_CMM_TYPE, reader);
+            setInt32(directory, IccDirectory.TAG_PROFILE_VERSION, reader);
+            set4ByteString(directory, IccDirectory.TAG_PROFILE_CLASS, reader);
+            set4ByteString(directory, IccDirectory.TAG_COLOR_SPACE, reader);
+            set4ByteString(directory, IccDirectory.TAG_PROFILE_CONNECTION_SPACE, reader);
+            setDate(directory, IccDirectory.TAG_PROFILE_DATETIME, reader);
+            set4ByteString(directory, IccDirectory.TAG_SIGNATURE, reader);
+            set4ByteString(directory, IccDirectory.TAG_PLATFORM, reader);
+            setInt32(directory, IccDirectory.TAG_CMM_FLAGS, reader);
+            set4ByteString(directory, IccDirectory.TAG_DEVICE_MAKE, reader);
+
+            int temp = reader.getInt32(IccDirectory.TAG_DEVICE_MODEL);
+            if (temp != 0) {
+                if (temp <= 0x20202020) {
+                    directory.setInt(IccDirectory.TAG_DEVICE_MODEL, temp);
+                } else {
+                    directory.setString(IccDirectory.TAG_DEVICE_MODEL, getStringFromInt32(temp));
+                }
+            }
+
+            setInt32(directory, IccDirectory.TAG_RENDERING_INTENT, reader);
+            setInt64(directory, IccDirectory.TAG_DEVICE_ATTR, reader);
+
+            float[] xyz = new float[]{
+                    reader.getS15Fixed16(IccDirectory.TAG_XYZ_VALUES),
+                    reader.getS15Fixed16(IccDirectory.TAG_XYZ_VALUES + 4),
+                    reader.getS15Fixed16(IccDirectory.TAG_XYZ_VALUES + 8)
+            };
+            directory.setObject(IccDirectory.TAG_XYZ_VALUES, xyz);
+
+            // Process 'ICC tags'
+            int tagCount = reader.getInt32(IccDirectory.TAG_TAG_COUNT);
+            directory.setInt(IccDirectory.TAG_TAG_COUNT, tagCount);
+
+            for (int i = 0; i < tagCount; i++) {
+                int pos = IccDirectory.TAG_TAG_COUNT + 4 + i * 12;
+                int tagType = reader.getInt32(pos);
+                int tagPtr = reader.getInt32(pos + 4);
+                int tagLen = reader.getInt32(pos + 8);
+                byte[] b = reader.getBytes(tagPtr, tagLen);
+                directory.setByteArray(tagType, b);
+            }
+        } catch (IOException ex) {
+            directory.addError("Exception reading ICC profile: " + ex.getMessage());
+        }
+
+        metadata.addDirectory(directory);
+    }
+
+    private void set4ByteString(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
+    {
+        int i = reader.getInt32(tagType);
+        if (i != 0)
+            directory.setString(tagType, getStringFromInt32(i));
+    }
+
+    private void setInt32(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
+    {
+        int i = reader.getInt32(tagType);
+        if (i != 0)
+            directory.setInt(tagType, i);
+    }
+
+    @SuppressWarnings({"SameParameterValue"})
+    private void setInt64(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
+    {
+        long l = reader.getInt64(tagType);
+        if (l != 0)
+            directory.setLong(tagType, l);
+    }
+
+    @SuppressWarnings({"SameParameterValue", "MagicConstant"})
+    private void setDate(@NotNull final IccDirectory directory, final int tagType, @NotNull RandomAccessReader reader) throws IOException
+    {
+        final int y = reader.getUInt16(tagType);
+        final int m = reader.getUInt16(tagType + 2);
+        final int d = reader.getUInt16(tagType + 4);
+        final int h = reader.getUInt16(tagType + 6);
+        final int M = reader.getUInt16(tagType + 8);
+        final int s = reader.getUInt16(tagType + 10);
+
+        if (DateUtil.isValidDate(y, m - 1, d) && DateUtil.isValidTime(h, M, s))
+        {
+            String dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", y, m, d, h, M, s);
+            directory.setString(tagType, dateString);
+        }
+        else
+        {
+            directory.addError(String.format(
+                "ICC data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d",
+                y, m, d, h, M, s));
+        }
+    }
+
+    @NotNull
+    public static String getStringFromInt32(int d)
+    {
+        // MSB
+        byte[] b = new byte[] {
+                (byte) ((d & 0xFF000000) >> 24),
+                (byte) ((d & 0x00FF0000) >> 16),
+                (byte) ((d & 0x0000FF00) >> 8),
+                (byte) ((d & 0x000000FF))
+        };
+        return new String(b);
+    }
+}
Index: trunk/src/com/drew/metadata/icc/package-info.java
===================================================================
--- trunk/src/com/drew/metadata/icc/package-info.java	(revision 15218)
+++ trunk/src/com/drew/metadata/icc/package-info.java	(revision 15218)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of ICC (International Color Consortium) profile metadata.
+ */
+package com.drew.metadata.icc;
Index: trunk/src/com/drew/metadata/photoshop/DuckyDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/DuckyDirectory.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/DuckyDirectory.java	(revision 15218)
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.TagDescriptor;
+
+import java.util.HashMap;
+
+/**
+ * Holds the data found in Photoshop "ducky" segments, created during Save-for-Web.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class DuckyDirectory extends Directory
+{
+    public static final int TAG_QUALITY = 1;
+    public static final int TAG_COMMENT = 2;
+    public static final int TAG_COPYRIGHT = 3;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_COMMENT, "Comment");
+        _tagNameMap.put(TAG_COPYRIGHT, "Copyright");
+    }
+
+    public DuckyDirectory()
+    {
+        this.setDescriptor(new TagDescriptor<DuckyDirectory>(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Ducky";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/DuckyReader.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/DuckyReader.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/DuckyReader.java	(revision 15218)
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.photoshop;
+
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.Charsets;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Reads Photoshop "ducky" segments, created during Save-for-Web.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class DuckyReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    private static final String JPEG_SEGMENT_PREAMBLE = "Ducky";
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.APPC);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
+
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the necessary preamble
+            if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equals(new String(segmentBytes, 0, preambleLength)))
+                continue;
+
+            extract(
+                new SequentialByteArrayReader(segmentBytes, preambleLength),
+                metadata);
+        }
+    }
+
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        DuckyDirectory directory = new DuckyDirectory();
+        metadata.addDirectory(directory);
+
+        try
+        {
+            while (true)
+            {
+                int tag = reader.getUInt16();
+
+                // End of Segment is marked with zero
+                if (tag == 0)
+                    break;
+
+                int length = reader.getUInt16();
+
+                switch (tag)
+                {
+                    case DuckyDirectory.TAG_QUALITY:
+                    {
+                        if (length != 4)
+                        {
+                            directory.addError("Unexpected length for the quality tag");
+                            return;
+                        }
+                        directory.setInt(tag, reader.getInt32());
+                        break;
+                    }
+                    case DuckyDirectory.TAG_COMMENT:
+                    case DuckyDirectory.TAG_COPYRIGHT:
+                    {
+                        reader.skip(4);
+                        directory.setStringValue(tag, reader.getStringValue(length - 4, Charsets.UTF_16BE));
+                        break;
+                    }
+                    default:
+                    {
+                        // Unexpected tag
+                        directory.setByteArray(tag, reader.getBytes(length));
+                        break;
+                    }
+                }
+            }
+        }
+        catch (IOException e)
+        {
+            directory.addError(e.getMessage());
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/Knot.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/Knot.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/Knot.java	(revision 15218)
@@ -0,0 +1,55 @@
+package com.drew.metadata.photoshop;
+
+
+/**
+ * Represents a knot created by Photoshop:
+ *
+ * <ul>
+ *   <li>Linked knot</li>
+ *   <li>Unlinked knot</li>
+ * </ul>
+ *
+ * @author Payton Garland
+ */
+public class Knot
+{
+    private final double[] _points = new double[6];
+    private final String _type;
+
+    public Knot(String type)
+    {
+        _type = type;
+    }
+
+    /**
+     * Add an individual coordinate value (x or y) to
+     * points array (6 points per knot)
+     *
+     * @param index location of point to be added in points
+     * @param point coordinate value to be added to points
+     */
+    public void setPoint(int index, double point)
+    {
+        _points[index] = point;
+    }
+
+    /**
+     * Get an individual coordinate value (x or y)
+     *
+     * @return an individual coordinate value
+     */
+    public double getPoint(int index)
+    {
+        return _points[index];
+    }
+
+    /**
+     * Get the type of knot (linked or unlinked)
+     *
+     * @return the type of knot
+     */
+    public String getType()
+    {
+        return this._type;
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PhotoshopDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PhotoshopDescriptor.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PhotoshopDescriptor.java	(revision 15218)
@@ -0,0 +1,488 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.Charsets;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+
+import static com.drew.metadata.photoshop.PhotoshopDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Yuri Binev
+ * @author Payton Garland
+ */
+@SuppressWarnings("WeakerAccess")
+public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
+{
+    public PhotoshopDescriptor(@NotNull PhotoshopDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_THUMBNAIL:
+            case TAG_THUMBNAIL_OLD:
+                return getThumbnailDescription(tagType);
+            case TAG_URL:
+            case TAG_XML:
+                return getSimpleString(tagType);
+            case TAG_IPTC:
+                return getBinaryDataString(tagType);
+            case TAG_SLICES:
+                return getSlicesDescription();
+            case TAG_VERSION:
+                return getVersionDescription();
+            case TAG_COPYRIGHT:
+                return getBooleanString(tagType);
+            case TAG_RESOLUTION_INFO:
+                return getResolutionInfoDescription();
+            case TAG_GLOBAL_ANGLE:
+            case TAG_GLOBAL_ALTITUDE:
+            case TAG_URL_LIST:
+            case TAG_SEED_NUMBER:
+                return get32BitNumberString(tagType);
+            case TAG_JPEG_QUALITY:
+                return getJpegQualityString();
+            case TAG_PRINT_SCALE:
+                return getPrintScaleDescription();
+            case TAG_PIXEL_ASPECT_RATIO:
+                return getPixelAspectRatioString();
+            case TAG_CLIPPING_PATH_NAME:
+                return getClippingPathNameString(tagType);
+            default:
+                if (tagType >= 0x07D0 && tagType <= 0x0BB6)
+                    return getPathString(tagType);
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getJpegQualityString()
+    {
+        try {
+            byte[] b = _directory.getByteArray(TAG_JPEG_QUALITY);
+
+            if (b == null)
+                return _directory.getString(TAG_JPEG_QUALITY);
+
+            RandomAccessReader reader = new ByteArrayReader(b);
+            int q = reader.getUInt16(0); // & 0xFFFF;
+            int f = reader.getUInt16(2); // & 0xFFFF;
+            int s = reader.getUInt16(4);
+
+            int q1 = q <= 0xFFFF && q >= 0xFFFD
+                ? q - 0xFFFC
+                : q <= 8
+                    ? q + 4
+                    : q;
+
+            String quality;
+            switch (q) {
+                case 0xFFFD:
+                case 0xFFFE:
+                case 0xFFFF:
+                case 0:
+                    quality = "Low";
+                    break;
+                case 1:
+                case 2:
+                case 3:
+                    quality = "Medium";
+                    break;
+                case 4:
+                case 5:
+                    quality = "High";
+                    break;
+                case 6:
+                case 7:
+                case 8:
+                    quality = "Maximum";
+                    break;
+                default:
+                    quality = "Unknown";
+            }
+
+            String format;
+            switch (f) {
+                case 0x0000:
+                    format = "Standard";
+                    break;
+                case 0x0001:
+                    format = "Optimised";
+                    break;
+                case 0x0101:
+                    format = "Progressive";
+                    break;
+                default:
+                    format = String.format("Unknown 0x%04X", f);
+            }
+
+            String scans = s >= 1 && s <= 3
+                    ? String.format("%d", s + 2)
+                    : String.format("Unknown 0x%04X", s);
+
+            return String.format("%d (%s), %s format, %s scans", q1, quality, format, scans);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getPixelAspectRatioString()
+    {
+        try {
+            byte[] bytes = _directory.getByteArray(TAG_PIXEL_ASPECT_RATIO);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            double d = reader.getDouble64(4);
+            return Double.toString(d);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getPrintScaleDescription()
+    {
+        try {
+            byte bytes[] = _directory.getByteArray(TAG_PRINT_SCALE);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            int style = reader.getInt32(0);
+            float locX = reader.getFloat32(2);
+            float locY = reader.getFloat32(6);
+            float scale = reader.getFloat32(10);
+            switch (style) {
+                case 0:
+                    return "Centered, Scale " + scale;
+                case 1:
+                    return "Size to fit";
+                case 2:
+                    return String.format("User defined, X:%s Y:%s, Scale:%s", locX, locY, scale);
+                default:
+                    return String.format("Unknown %04X, X:%s Y:%s, Scale:%s", style, locX, locY, scale);
+            }
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getResolutionInfoDescription()
+    {
+        try {
+            byte[] bytes = _directory.getByteArray(TAG_RESOLUTION_INFO);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            float resX = reader.getS15Fixed16(0);
+            float resY = reader.getS15Fixed16(8); // is this the correct offset? it's only reading 4 bytes each time
+            DecimalFormat format = new DecimalFormat("0.##");
+            return format.format(resX) + "x" + format.format(resY) + " DPI";
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getVersionDescription()
+    {
+        try {
+            final byte[] bytes = _directory.getByteArray(TAG_VERSION);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            int pos = 0;
+            int ver = reader.getInt32(0);
+            pos += 4;
+            pos++;
+            int readerLength = reader.getInt32(5);
+            pos += 4;
+            String readerStr = reader.getString(9, readerLength * 2, "UTF-16");
+            pos += readerLength * 2;
+            int writerLength = reader.getInt32(pos);
+            pos += 4;
+            String writerStr = reader.getString(pos, writerLength * 2, "UTF-16");
+            pos += writerLength * 2;
+            int fileVersion = reader.getInt32(pos);
+            return String.format("%d (%s, %s) %d", ver, readerStr, writerStr, fileVersion);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getSlicesDescription()
+    {
+        try {
+            final byte bytes[] = _directory.getByteArray(TAG_SLICES);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            int nameLength = reader.getInt32(20);
+            String name = reader.getString(24, nameLength * 2, "UTF-16");
+            int pos = 24 + nameLength * 2;
+            int sliceCount = reader.getInt32(pos);
+            return String.format("%s (%d,%d,%d,%d) %d Slices",
+                    name, reader.getInt32(4), reader.getInt32(8), reader.getInt32(12), reader.getInt32(16), sliceCount);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getThumbnailDescription(int tagType)
+    {
+        try {
+            byte[] v = _directory.getByteArray(tagType);
+            if (v == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(v);
+            int format = reader.getInt32(0);
+            int width = reader.getInt32(4);
+            int height = reader.getInt32(8);
+            //skip WidthBytes
+            int totalSize = reader.getInt32(16);
+            int compSize = reader.getInt32(20);
+            int bpp = reader.getInt32(24);
+            //skip Number of planes
+            return String.format("%s, %dx%d, Decomp %d bytes, %d bpp, %d bytes",
+                    format == 1 ? "JpegRGB" : "RawRGB",
+                    width, height, totalSize, bpp, compSize);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    private String getBooleanString(int tag)
+    {
+        final byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null || bytes.length == 0)
+            return null;
+        return bytes[0] == 0 ? "No" : "Yes";
+    }
+
+    @Nullable
+    private String get32BitNumberString(int tag)
+    {
+        byte[] bytes = _directory.getByteArray(tag);
+        if (bytes == null)
+            return null;
+        RandomAccessReader reader = new ByteArrayReader(bytes);
+        try {
+            return String.format("%d", reader.getInt32(0));
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    private String getSimpleString(int tagType)
+    {
+        final byte[] bytes = _directory.getByteArray(tagType);
+        if (bytes == null)
+            return null;
+        return new String(bytes);
+    }
+
+    @Nullable
+    private String getBinaryDataString(int tagType)
+    {
+        final byte[] bytes = _directory.getByteArray(tagType);
+        if (bytes == null)
+            return null;
+        return String.format("%d bytes binary data", bytes.length);
+    }
+
+    @Nullable
+    public String getClippingPathNameString(int tagType)
+    {
+        try {
+            byte[] bytes = _directory.getByteArray(tagType);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            int length = reader.getByte(0);
+            return new String(reader.getBytes(1, length), "UTF-8");
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getPathString(int tagType)
+    {
+        try {
+            byte[] bytes = _directory.getByteArray(tagType);
+            if (bytes == null)
+                return null;
+            RandomAccessReader reader = new ByteArrayReader(bytes);
+            int length = (int) (reader.getLength() - reader.getByte((int)reader.getLength() - 1) - 1) / 26;
+
+            String fillRecord = null;
+
+            // Possible subpaths
+            Subpath cSubpath = new Subpath();
+            Subpath oSubpath = new Subpath();
+
+            ArrayList<Subpath> paths = new ArrayList<Subpath>();
+
+            // Loop through each path resource block segment (26-bytes)
+            for (int i = 0; i < length; i++) {
+                // Spacer takes into account which block is currently being worked on while accessing byte array
+                int recordSpacer = 26 * i;
+                int selector = reader.getInt16(recordSpacer);
+
+                /*
+                 * Subpath resource blocks come in 26-byte segments with 9 possible selectors - some selectors
+                 * are formatted different from others
+                 *
+                 *      0 = Closed subpath length record
+                 *      1 = Closed subpath Bezier knot, linked
+                 *      2 = Closed subpath Bezier knot, unlinked
+                 *      3 = Open subpath length record
+                 *      4 = Open subpath Bezier knot, linked
+                 *      5 = Open subpath Bezier knot, unlinked
+                 *      6 = Subpath fill rule record
+                 *      7 = Clipboard record
+                 *      8 = Initial fill rule record
+                 *
+                 * Source: http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
+                 */
+                switch (selector) {
+                    case 0:
+                        // Insert previous Paths if there are any
+                        if (cSubpath.size() != 0) {
+                            paths.add(cSubpath);
+                        }
+
+                        // Make path size accordingly
+                        cSubpath = new Subpath("Closed Subpath");
+                        break;
+                    case 1:
+                    case 2:
+                    {
+                        Knot knot;
+                        if (selector == 1)
+                            knot = new Knot("Linked");
+                        else
+                            knot = new Knot("Unlinked");
+                        // Insert each point into cSubpath - points are 32-bit signed, fixed point numbers and have 8-bits before the point
+                        for (int j = 0; j < 6; j++) {
+                            knot.setPoint(j, reader.getInt8((j * 4) + 2 + recordSpacer) + (reader.getInt24((j * 4) + 3 + recordSpacer) / Math.pow(2.0, 24.0)));
+                        }
+                        cSubpath.add(knot);
+                        break;
+                    }
+                    case 3:
+                        // Insert previous Paths if there are any
+                        if (oSubpath.size() != 0) {
+                            paths.add(oSubpath);
+                        }
+
+                        // Make path size accordingly
+                        oSubpath = new Subpath("Open Subpath");
+                        break;
+                    case 4:
+                    case 5:
+                    {
+                        Knot knot;
+                        if (selector == 4)
+                            knot = new Knot("Linked");
+                        else
+                            knot = new Knot("Unlinked");
+                        // Insert each point into oSubpath - points are 32-bit signed, fixed point numbers and have 8-bits before the point
+                        for (int j = 0; j < 6; j++) {
+                            knot.setPoint(j, reader.getInt8((j * 4) + 2 + recordSpacer) + (reader.getInt24((j * 4) + 3 + recordSpacer) / Math.pow(2.0, 24.0)));
+                        }
+                        oSubpath.add(knot);
+                        break;
+                    }
+                    case 6:
+                        break;
+                    case 7:
+                        // TODO: Clipboard record
+//                        for (int j = 0; j < 24; j++) {
+//                           clipboardRecord[j] = bytes[j + 2 + recordSpacer];
+//                        }
+                        break;
+                    case 8:
+                        if (reader.getInt16(2 + recordSpacer) == 1)
+                            fillRecord = "with all pixels";
+                        else
+                            fillRecord = "without all pixels";
+                        break;
+                }
+            }
+
+            // Add any more paths that were not added already
+            if (cSubpath.size() != 0)
+                paths.add(cSubpath);
+            if (oSubpath.size() != 0)
+                paths.add(oSubpath);
+
+            // Extract name (previously appended to end of byte array)
+            int nameLength = reader.getByte((int)reader.getLength() - 1);
+            String name = reader.getString((int)reader.getLength() - nameLength - 1, nameLength, Charsets.ASCII);
+
+            // Build description
+            StringBuilder str = new StringBuilder();
+
+            str.append('"').append(name).append('"')
+                .append(" having ");
+
+            if (fillRecord != null)
+                str.append("initial fill rule \"").append(fillRecord).append("\" and ");
+
+            str.append(paths.size()).append(paths.size() == 1 ? " subpath:" : " subpaths:");
+
+            for (Subpath path : paths) {
+                str.append("\n- ").append(path.getType()).append(" with ").append(paths.size()).append(paths.size() == 1 ? " knot:" : " knots:");
+
+                for (Knot knot : path.getKnots()) {
+                    str.append("\n  - ").append(knot.getType());
+                    str.append(" (").append(knot.getPoint(0)).append(",").append(knot.getPoint(1)).append(")");
+                    str.append(" (").append(knot.getPoint(2)).append(",").append(knot.getPoint(3)).append(")");
+                    str.append(" (").append(knot.getPoint(4)).append(",").append(knot.getPoint(5)).append(")");
+                }
+            }
+
+            return str.toString();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PhotoshopDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PhotoshopDirectory.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PhotoshopDirectory.java	(revision 15218)
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Holds the metadata found in the APPD segment of a JPEG file saved by Photoshop.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Yuri Binev
+ * @author Payton Garland
+ */
+@SuppressWarnings("WeakerAccess")
+public class PhotoshopDirectory extends Directory
+{
+    public static final int TAG_CHANNELS_ROWS_COLUMNS_DEPTH_MODE                  = 0x03E8;
+    public static final int TAG_MAC_PRINT_INFO                                    = 0x03E9;
+    public static final int TAG_XML                                               = 0x03EA;
+    public static final int TAG_INDEXED_COLOR_TABLE                               = 0x03EB;
+    public static final int TAG_RESOLUTION_INFO                                   = 0x03ED;
+    public static final int TAG_ALPHA_CHANNELS                                    = 0x03EE;
+    public static final int TAG_DISPLAY_INFO_OBSOLETE                             = 0x03EF;
+    public static final int TAG_CAPTION                                           = 0x03F0;
+    public static final int TAG_BORDER_INFORMATION                                = 0x03F1;
+    public static final int TAG_BACKGROUND_COLOR                                  = 0x03F2;
+    public static final int TAG_PRINT_FLAGS                                       = 0x03F3;
+    public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_HALFTONING_INFORMATION = 0x03F4;
+    public static final int TAG_COLOR_HALFTONING_INFORMATION                      = 0x03F5;
+    public static final int TAG_DUOTONE_HALFTONING_INFORMATION                    = 0x03F6;
+    public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_TRANSFER_FUNCTION      = 0x03F7;
+    public static final int TAG_COLOR_TRANSFER_FUNCTIONS                          = 0x03F8;
+    public static final int TAG_DUOTONE_TRANSFER_FUNCTIONS                        = 0x03F9;
+    public static final int TAG_DUOTONE_IMAGE_INFORMATION                         = 0x03FA;
+    public static final int TAG_EFFECTIVE_BLACK_AND_WHITE_VALUES                  = 0x03FB;
+    // OBSOLETE                                                                     0x03FC
+    public static final int TAG_EPS_OPTIONS                                       = 0x03FD;
+    public static final int TAG_QUICK_MASK_INFORMATION                            = 0x03FE;
+    // OBSOLETE                                                                     0x03FF
+    public static final int TAG_LAYER_STATE_INFORMATION                           = 0x0400;
+    // Working path (not saved)                                                     0x0401
+    public static final int TAG_LAYERS_GROUP_INFORMATION                          = 0x0402;
+    // OBSOLETE                                                                     0x0403
+    public static final int TAG_IPTC                                              = 0x0404;
+    public static final int TAG_IMAGE_MODE_FOR_RAW_FORMAT_FILES                   = 0x0405;
+    public static final int TAG_JPEG_QUALITY                                      = 0x0406;
+    public static final int TAG_GRID_AND_GUIDES_INFORMATION                       = 0x0408;
+    public static final int TAG_THUMBNAIL_OLD                                     = 0x0409;
+    public static final int TAG_COPYRIGHT                                         = 0x040A;
+    public static final int TAG_URL                                               = 0x040B;
+    public static final int TAG_THUMBNAIL                                         = 0x040C;
+    public static final int TAG_GLOBAL_ANGLE                                      = 0x040D;
+    // OBSOLETE                                                                     0x040E
+    public static final int TAG_ICC_PROFILE_BYTES                                 = 0x040F;
+    public static final int TAG_WATERMARK                                         = 0x0410;
+    public static final int TAG_ICC_UNTAGGED_PROFILE                              = 0x0411;
+    public static final int TAG_EFFECTS_VISIBLE                                   = 0x0412;
+    public static final int TAG_SPOT_HALFTONE                                     = 0x0413;
+    public static final int TAG_SEED_NUMBER                                       = 0x0414;
+    public static final int TAG_UNICODE_ALPHA_NAMES                               = 0x0415;
+    public static final int TAG_INDEXED_COLOR_TABLE_COUNT                         = 0x0416;
+    public static final int TAG_TRANSPARENCY_INDEX                                = 0x0417;
+    public static final int TAG_GLOBAL_ALTITUDE                                   = 0x0419;
+    public static final int TAG_SLICES                                            = 0x041A;
+    public static final int TAG_WORKFLOW_URL                                      = 0x041B;
+    public static final int TAG_JUMP_TO_XPEP                                      = 0x041C;
+    public static final int TAG_ALPHA_IDENTIFIERS                                 = 0x041D;
+    public static final int TAG_URL_LIST                                          = 0x041E;
+    public static final int TAG_VERSION                                           = 0x0421;
+    public static final int TAG_EXIF_DATA_1                                       = 0x0422;
+    public static final int TAG_EXIF_DATA_3                                       = 0x0423;
+    public static final int TAG_XMP_DATA                                          = 0x0424;
+    public static final int TAG_CAPTION_DIGEST                                    = 0x0425;
+    public static final int TAG_PRINT_SCALE                                       = 0x0426;
+    public static final int TAG_PIXEL_ASPECT_RATIO                                = 0x0428;
+    public static final int TAG_LAYER_COMPS                                       = 0x0429;
+    public static final int TAG_ALTERNATE_DUOTONE_COLORS                          = 0x042A;
+    public static final int TAG_ALTERNATE_SPOT_COLORS                             = 0x042B;
+    public static final int TAG_LAYER_SELECTION_IDS                               = 0x042D;
+    public static final int TAG_HDR_TONING_INFO                                   = 0x042E;
+    public static final int TAG_PRINT_INFO                                        = 0x042F;
+    public static final int TAG_LAYER_GROUPS_ENABLED_ID                           = 0x0430;
+    public static final int TAG_COLOR_SAMPLERS                                    = 0x0431;
+    public static final int TAG_MEASUREMENT_SCALE                                 = 0x0432;
+    public static final int TAG_TIMELINE_INFORMATION                              = 0x0433;
+    public static final int TAG_SHEET_DISCLOSURE                                  = 0x0434;
+    public static final int TAG_DISPLAY_INFO                                      = 0x0435;
+    public static final int TAG_ONION_SKINS                                       = 0x0436;
+    public static final int TAG_COUNT_INFORMATION                                 = 0x0438;
+    public static final int TAG_PRINT_INFO_2                                      = 0x043A;
+    public static final int TAG_PRINT_STYLE                                       = 0x043B;
+    public static final int TAG_MAC_NSPRINTINFO                                   = 0x043C;
+    public static final int TAG_WIN_DEVMODE                                       = 0x043D;
+    public static final int TAG_AUTO_SAVE_FILE_PATH                               = 0x043E;
+    public static final int TAG_AUTO_SAVE_FORMAT                                  = 0x043F;
+    public static final int TAG_PATH_SELECTION_STATE                              = 0x0440;
+    // PATH INFO                                                                    0x07D0 -> 0x0BB6
+    public static final int TAG_CLIPPING_PATH_NAME                                = 0x0BB7;
+    public static final int TAG_ORIGIN_PATH_INFO                                  = 0x0BB8;
+    // PLUG IN RESOURCES                                                            0x0FA0 -> 0x1387
+    public static final int TAG_IMAGE_READY_VARIABLES_XML                         = 0x1B58;
+    public static final int TAG_IMAGE_READY_DATA_SETS                             = 0x1B59;
+    public static final int TAG_IMAGE_READY_SELECTED_STATE                        = 0x1B5A;
+    public static final int TAG_IMAGE_READY_7_ROLLOVER                            = 0x1B5B;
+    public static final int TAG_IMAGE_READY_ROLLOVER                              = 0x1B5C;
+    public static final int TAG_IMAGE_READY_SAVE_LAYER_SETTINGS                   = 0x1B5D;
+    public static final int TAG_IMAGE_READY_VERSION                               = 0x1B5E;
+    public static final int TAG_LIGHTROOM_WORKFLOW                                = 0x1F40;
+    public static final int TAG_PRINT_FLAGS_INFO                                  = 0x2710;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_CHANNELS_ROWS_COLUMNS_DEPTH_MODE, "Channels, Rows, Columns, Depth, Mode");
+        _tagNameMap.put(TAG_MAC_PRINT_INFO, "Mac Print Info");
+        _tagNameMap.put(TAG_XML, "XML Data");
+        _tagNameMap.put(TAG_INDEXED_COLOR_TABLE, "Indexed Color Table");
+        _tagNameMap.put(TAG_RESOLUTION_INFO, "Resolution Info");
+        _tagNameMap.put(TAG_ALPHA_CHANNELS, "Alpha Channels");
+        _tagNameMap.put(TAG_DISPLAY_INFO_OBSOLETE, "Display Info (Obsolete)");
+        _tagNameMap.put(TAG_CAPTION, "Caption");
+        _tagNameMap.put(TAG_BORDER_INFORMATION, "Border Information");
+        _tagNameMap.put(TAG_BACKGROUND_COLOR, "Background Color");
+        _tagNameMap.put(TAG_PRINT_FLAGS, "Print Flags");
+        _tagNameMap.put(TAG_GRAYSCALE_AND_MULTICHANNEL_HALFTONING_INFORMATION, "Grayscale and Multichannel Halftoning Information");
+        _tagNameMap.put(TAG_COLOR_HALFTONING_INFORMATION, "Color Halftoning Information");
+        _tagNameMap.put(TAG_DUOTONE_HALFTONING_INFORMATION, "Duotone Halftoning Information");
+        _tagNameMap.put(TAG_GRAYSCALE_AND_MULTICHANNEL_TRANSFER_FUNCTION, "Grayscale and Multichannel Transfer Function");
+        _tagNameMap.put(TAG_COLOR_TRANSFER_FUNCTIONS, "Color Transfer Functions");
+        _tagNameMap.put(TAG_DUOTONE_TRANSFER_FUNCTIONS, "Duotone Transfer Functions");
+        _tagNameMap.put(TAG_DUOTONE_IMAGE_INFORMATION, "Duotone Image Information");
+        _tagNameMap.put(TAG_EFFECTIVE_BLACK_AND_WHITE_VALUES, "Effective Black and White Values");
+        _tagNameMap.put(TAG_EPS_OPTIONS, "EPS Options");
+        _tagNameMap.put(TAG_QUICK_MASK_INFORMATION, "Quick Mask Information");
+        _tagNameMap.put(TAG_LAYER_STATE_INFORMATION, "Layer State Information");
+        _tagNameMap.put(TAG_LAYERS_GROUP_INFORMATION, "Layers Group Information");
+        _tagNameMap.put(TAG_IPTC, "IPTC-NAA Record");
+        _tagNameMap.put(TAG_IMAGE_MODE_FOR_RAW_FORMAT_FILES, "Image Mode for Raw Format Files");
+        _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality");
+        _tagNameMap.put(TAG_GRID_AND_GUIDES_INFORMATION, "Grid and Guides Information");
+        _tagNameMap.put(TAG_THUMBNAIL_OLD, "Photoshop 4.0 Thumbnail");
+        _tagNameMap.put(TAG_COPYRIGHT, "Copyright Flag");
+        _tagNameMap.put(TAG_URL, "URL");
+        _tagNameMap.put(TAG_THUMBNAIL, "Thumbnail Data");
+        _tagNameMap.put(TAG_GLOBAL_ANGLE, "Global Angle");
+        _tagNameMap.put(TAG_ICC_PROFILE_BYTES, "ICC Profile Bytes");
+        _tagNameMap.put(TAG_WATERMARK, "Watermark");
+        _tagNameMap.put(TAG_ICC_UNTAGGED_PROFILE, "ICC Untagged Profile");
+        _tagNameMap.put(TAG_EFFECTS_VISIBLE, "Effects Visible");
+        _tagNameMap.put(TAG_SPOT_HALFTONE, "Spot Halftone");
+        _tagNameMap.put(TAG_SEED_NUMBER, "Seed Number");
+        _tagNameMap.put(TAG_UNICODE_ALPHA_NAMES, "Unicode Alpha Names");
+        _tagNameMap.put(TAG_INDEXED_COLOR_TABLE_COUNT, "Indexed Color Table Count");
+        _tagNameMap.put(TAG_TRANSPARENCY_INDEX, "Transparency Index");
+        _tagNameMap.put(TAG_GLOBAL_ALTITUDE, "Global Altitude");
+        _tagNameMap.put(TAG_SLICES, "Slices");
+        _tagNameMap.put(TAG_WORKFLOW_URL, "Workflow URL");
+        _tagNameMap.put(TAG_JUMP_TO_XPEP, "Jump To XPEP");
+        _tagNameMap.put(TAG_ALPHA_IDENTIFIERS, "Alpha Identifiers");
+        _tagNameMap.put(TAG_URL_LIST, "URL List");
+        _tagNameMap.put(TAG_VERSION, "Version Info");
+        _tagNameMap.put(TAG_EXIF_DATA_1, "EXIF Data 1");
+        _tagNameMap.put(TAG_EXIF_DATA_3, "EXIF Data 3");
+        _tagNameMap.put(TAG_XMP_DATA, "XMP Data");
+        _tagNameMap.put(TAG_CAPTION_DIGEST, "Caption Digest");
+        _tagNameMap.put(TAG_PRINT_SCALE, "Print Scale");
+        _tagNameMap.put(TAG_PIXEL_ASPECT_RATIO, "Pixel Aspect Ratio");
+        _tagNameMap.put(TAG_LAYER_COMPS, "Layer Comps");
+        _tagNameMap.put(TAG_ALTERNATE_DUOTONE_COLORS, "Alternate Duotone Colors");
+        _tagNameMap.put(TAG_ALTERNATE_SPOT_COLORS, "Alternate Spot Colors");
+        _tagNameMap.put(TAG_LAYER_SELECTION_IDS, "Layer Selection IDs");
+        _tagNameMap.put(TAG_HDR_TONING_INFO, "HDR Toning Info");
+        _tagNameMap.put(TAG_PRINT_INFO, "Print Info");
+        _tagNameMap.put(TAG_LAYER_GROUPS_ENABLED_ID, "Layer Groups Enabled ID");
+        _tagNameMap.put(TAG_COLOR_SAMPLERS, "Color Samplers");
+        _tagNameMap.put(TAG_MEASUREMENT_SCALE, "Measurement Scale");
+        _tagNameMap.put(TAG_TIMELINE_INFORMATION, "Timeline Information");
+        _tagNameMap.put(TAG_SHEET_DISCLOSURE, "Sheet Disclosure");
+        _tagNameMap.put(TAG_DISPLAY_INFO, "Display Info");
+        _tagNameMap.put(TAG_ONION_SKINS, "Onion Skins");
+        _tagNameMap.put(TAG_COUNT_INFORMATION, "Count information");
+        _tagNameMap.put(TAG_PRINT_INFO_2, "Print Info 2");
+        _tagNameMap.put(TAG_PRINT_STYLE, "Print Style");
+        _tagNameMap.put(TAG_MAC_NSPRINTINFO, "Mac NSPrintInfo");
+        _tagNameMap.put(TAG_WIN_DEVMODE, "Win DEVMODE");
+        _tagNameMap.put(TAG_AUTO_SAVE_FILE_PATH, "Auto Save File Subpath");
+        _tagNameMap.put(TAG_AUTO_SAVE_FORMAT, "Auto Save Format");
+        _tagNameMap.put(TAG_PATH_SELECTION_STATE, "Subpath Selection State");
+
+        _tagNameMap.put(TAG_CLIPPING_PATH_NAME, "Clipping Path Name");
+        _tagNameMap.put(TAG_ORIGIN_PATH_INFO, "Origin Subpath Info");
+        _tagNameMap.put(TAG_IMAGE_READY_VARIABLES_XML, "Image Ready Variables XML");
+        _tagNameMap.put(TAG_IMAGE_READY_DATA_SETS, "Image Ready Data Sets");
+        _tagNameMap.put(TAG_IMAGE_READY_SELECTED_STATE, "Image Ready Selected State");
+        _tagNameMap.put(TAG_IMAGE_READY_7_ROLLOVER, "Image Ready 7 Rollover Expanded State");
+        _tagNameMap.put(TAG_IMAGE_READY_ROLLOVER, "Image Ready Rollover Expanded State");
+        _tagNameMap.put(TAG_IMAGE_READY_SAVE_LAYER_SETTINGS, "Image Ready Save Layer Settings");
+        _tagNameMap.put(TAG_IMAGE_READY_VERSION, "Image Ready Version");
+        _tagNameMap.put(TAG_LIGHTROOM_WORKFLOW, "Lightroom Workflow");
+        _tagNameMap.put(TAG_PRINT_FLAGS_INFO, "Print Flags Information");
+    }
+
+    public PhotoshopDirectory()
+    {
+        this.setDescriptor(new PhotoshopDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "Photoshop";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    @Nullable
+    public byte[] getThumbnailBytes()
+    {
+        byte[] storedBytes = getByteArray(PhotoshopDirectory.TAG_THUMBNAIL);
+        if (storedBytes == null)
+            storedBytes = getByteArray(PhotoshopDirectory.TAG_THUMBNAIL_OLD);
+        if (storedBytes == null || storedBytes.length <= 28)
+            return null;
+
+        int thumbSize = storedBytes.length - 28;
+        byte[] thumbBytes = new byte[thumbSize];
+        System.arraycopy(storedBytes, 28, thumbBytes, 0, thumbSize);
+        return thumbBytes;
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PhotoshopReader.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PhotoshopReader.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PhotoshopReader.java	(revision 15218)
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+package com.drew.metadata.photoshop;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import com.drew.imaging.ImageProcessingException;
+import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
+import com.drew.imaging.jpeg.JpegSegmentType;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.exif.ExifReader;
+import com.drew.metadata.icc.IccReader;
+import com.drew.metadata.iptc.IptcReader;
+//import com.drew.metadata.xmp.XmpReader;
+
+/**
+ * Reads metadata created by Photoshop and stored in the APPD segment of JPEG files.
+ * Note that IPTC data may be stored within this segment, in which case this reader will
+ * create both a {@link PhotoshopDirectory} and a {@link com.drew.metadata.iptc.IptcDirectory}.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ * @author Yuri Binev
+ * @author Payton Garland
+ */
+public class PhotoshopReader implements JpegSegmentMetadataReader
+{
+    @NotNull
+    private static final String JPEG_SEGMENT_PREAMBLE = "Photoshop 3.0";
+
+    @NotNull
+    public Iterable<JpegSegmentType> getSegmentTypes()
+    {
+        return Collections.singletonList(JpegSegmentType.APPD);
+    }
+
+    public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
+    {
+        final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
+
+        for (byte[] segmentBytes : segments) {
+            // Ensure data starts with the necessary preamble
+            if (segmentBytes.length < preambleLength + 1 || !JPEG_SEGMENT_PREAMBLE.equals(new String(segmentBytes, 0, preambleLength)))
+                continue;
+
+            extract(
+                new SequentialByteArrayReader(segmentBytes, preambleLength + 1),
+                segmentBytes.length - preambleLength - 1,
+                metadata);
+        }
+    }
+
+    public void extract(@NotNull final SequentialReader reader, int length, @NotNull final Metadata metadata)
+    {
+        extract(reader, length, metadata, null);
+    }
+
+    public void extract(@NotNull final SequentialReader reader, int length, @NotNull final Metadata metadata, @Nullable final Directory parentDirectory)
+    {
+        PhotoshopDirectory directory = new PhotoshopDirectory();
+        metadata.addDirectory(directory);
+
+        if (parentDirectory != null)
+            directory.setParent(parentDirectory);
+
+        // Data contains a sequence of Image Resource Blocks (IRBs):
+        //
+        // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found
+        // 2 bytes - Resource identifier
+        // String  - Pascal string, padded to make length even
+        // 4 bytes - Size of resource data which follows
+        // Data    - The resource data, padded to make size even
+        //
+        // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
+
+        int pos = 0;
+        int clippingPathCount = 0;
+        while (pos < length) {
+            try {
+                // 4 bytes for the signature ("8BIM", "PHUT", etc.)
+                String signature = reader.getString(4);
+                pos += 4;
+
+                // 2 bytes for the resource identifier (tag type).
+                int tagType = reader.getUInt16(); // segment type
+                pos += 2;
+
+                // A variable number of bytes holding a pascal string (two leading bytes for length).
+                short descriptionLength = reader.getUInt8();
+                pos += 1;
+                // Some basic bounds checking
+                if (descriptionLength < 0 || descriptionLength + pos > length)
+                    throw new ImageProcessingException("Invalid string length");
+
+                // Get name (important for paths)
+                StringBuilder description = new StringBuilder();
+                descriptionLength += pos;
+                // Loop through each byte and append to string
+                while (pos < descriptionLength) {
+                    description.append((char)reader.getUInt8());
+                    pos ++;
+                }
+
+                // The number of bytes is padded with a trailing zero, if needed, to make the size even.
+                if (pos % 2 != 0) {
+                    reader.skip(1);
+                    pos++;
+                }
+
+                // 4 bytes for the size of the resource data that follows.
+                int byteCount = reader.getInt32();
+                pos += 4;
+                // The resource data.
+                byte[] tagBytes = reader.getBytes(byteCount);
+                pos += byteCount;
+                // The number of bytes is padded with a trailing zero, if needed, to make the size even.
+                if (pos % 2 != 0) {
+                    reader.skip(1);
+                    pos++;
+                }
+
+                if (signature.equals("8BIM")) {
+                    if (tagType == PhotoshopDirectory.TAG_IPTC)
+                        new IptcReader().extract(new SequentialByteArrayReader(tagBytes), metadata, tagBytes.length, directory);
+                    else if (tagType == PhotoshopDirectory.TAG_ICC_PROFILE_BYTES)
+                        new IccReader().extract(new ByteArrayReader(tagBytes), metadata, directory);
+                    else if (tagType == PhotoshopDirectory.TAG_EXIF_DATA_1 || tagType == PhotoshopDirectory.TAG_EXIF_DATA_3)
+                        new ExifReader().extract(new ByteArrayReader(tagBytes), metadata, 0, directory);
+                    //else if (tagType == PhotoshopDirectory.TAG_XMP_DATA)
+                    //    new XmpReader().extract(tagBytes, metadata, directory);
+                    else if (tagType >= 0x07D0 && tagType <= 0x0BB6) {
+                        clippingPathCount++;
+                        tagBytes = Arrays.copyOf(tagBytes, tagBytes.length + description.length() + 1);
+                        // Append description(name) to end of byte array with 1 byte before the description representing the length
+                        for (int i = tagBytes.length - description.length() - 1; i < tagBytes.length; i++) {
+                            if (i % (tagBytes.length - description.length() - 1 + description.length()) == 0)
+                                tagBytes[i] = (byte)description.length();
+                            else
+                                tagBytes[i] = (byte)description.charAt(i - (tagBytes.length - description.length() - 1));
+                        }
+                        PhotoshopDirectory._tagNameMap.put(0x07CF + clippingPathCount, "Path Info " + clippingPathCount);
+                        directory.setByteArray(0x07CF + clippingPathCount, tagBytes);
+                    }
+                    else
+                        directory.setByteArray(tagType, tagBytes);
+
+                    if (tagType >= 0x0fa0 && tagType <= 0x1387)
+                        PhotoshopDirectory._tagNameMap.put(tagType, String.format("Plug-in %d Data", tagType - 0x0fa0 + 1));
+                }
+            } catch (Exception ex) {
+                directory.addError(ex.getMessage());
+                return;
+            }
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PhotoshopTiffHandler.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PhotoshopTiffHandler.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PhotoshopTiffHandler.java	(revision 15218)
@@ -0,0 +1,62 @@
+package com.drew.metadata.photoshop;
+
+import java.io.IOException;
+import java.util.Set;
+
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.RandomAccessReader;
+import com.drew.lang.SequentialByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.exif.ExifTiffHandler;
+import com.drew.metadata.icc.IccReader;
+//import com.drew.metadata.xmp.XmpReader;
+
+/**
+ * @author Payton Garland
+ */
+public class PhotoshopTiffHandler extends ExifTiffHandler
+{
+    // Photoshop-specific Tiff Tags
+    // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1039502
+    private static final int TAG_PAGE_MAKER_EXTENSION = 0x014A;
+    private static final int TAG_JPEG_TABLES = 0X01B5;
+    private static final int TAG_XMP = 0x02BC;
+    private static final int TAG_FILE_INFORMATION = 0x83BB;
+    private static final int TAG_PHOTOSHOP_IMAGE_RESOURCES = 0x8649;
+    private static final int TAG_EXIF_IFD_POINTER = 0x8769;
+    private static final int TAG_ICC_PROFILES = 0x8773;
+    private static final int TAG_EXIF_GPS = 0x8825;
+    private static final int TAG_T_IMAGE_SOURCE_DATA = 0x935C;
+    private static final int TAG_T_ANNOTATIONS = 0xC44F;
+
+    public PhotoshopTiffHandler(Metadata metadata, Directory parentDirectory)
+    {
+        super(metadata, parentDirectory);
+    }
+
+    @Override
+    public boolean customProcessTag(final int tagOffset,
+                                    final @NotNull Set<Integer> processedIfdOffsets,
+                                    final int tiffHeaderOffset,
+                                    final @NotNull RandomAccessReader reader,
+                                    final int tagId,
+                                    final int byteCount) throws IOException
+    {
+        switch (tagId) {
+            //case TAG_XMP:
+            //    new XmpReader().extract(reader.getBytes(tagOffset, byteCount), _metadata);
+            //    return true;
+            case TAG_PHOTOSHOP_IMAGE_RESOURCES:
+                new PhotoshopReader().extract(new SequentialByteArrayReader(reader.getBytes(tagOffset, byteCount)), byteCount, _metadata);
+                return true;
+            case TAG_ICC_PROFILES:
+                new IccReader().extract(new ByteArrayReader(reader.getBytes(tagOffset, byteCount)), _metadata);
+                return true;
+        }
+
+
+        return super.customProcessTag(tagOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, byteCount);
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PsdHeaderDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PsdHeaderDescriptor.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PsdHeaderDescriptor.java	(revision 15218)
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import static com.drew.metadata.photoshop.PsdHeaderDirectory.*;
+
+/**
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory>
+{
+    public PsdHeaderDescriptor(@NotNull PsdHeaderDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Override
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case TAG_CHANNEL_COUNT:
+                return getChannelCountDescription();
+            case TAG_BITS_PER_CHANNEL:
+                return getBitsPerChannelDescription();
+            case TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case TAG_IMAGE_HEIGHT:
+                return getImageHeightDescription();
+            case TAG_IMAGE_WIDTH:
+                return getImageWidthDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getChannelCountDescription()
+    {
+        // Supported range is 1 to 56.
+        Integer value = _directory.getInteger(TAG_CHANNEL_COUNT);
+        if (value == null)
+            return null;
+        return value + " channel" + (value == 1 ? "" : "s");
+    }
+
+    @Nullable
+    public String getBitsPerChannelDescription()
+    {
+        // Supported values are 1, 8, 16 and 32.
+        Integer value = _directory.getInteger(TAG_BITS_PER_CHANNEL);
+        if (value == null)
+            return null;
+        return value + " bit" + (value == 1 ? "" : "s") + " per channel";
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        return getIndexedDescription(TAG_COLOR_MODE,
+            "Bitmap",
+            "Grayscale",
+            "Indexed",
+            "RGB",
+            "CMYK",
+            null,
+            null,
+            "Multichannel",
+            "Duotone",
+            "Lab");
+    }
+
+    @Nullable
+    public String getImageHeightDescription()
+    {
+        Integer value = _directory.getInteger(TAG_IMAGE_HEIGHT);
+        if (value == null)
+            return null;
+        return value + " pixel" + (value == 1 ? "" : "s");
+    }
+
+    @Nullable
+    public String getImageWidthDescription()
+    {
+        try {
+            Integer value = _directory.getInteger(TAG_IMAGE_WIDTH);
+            if (value == null)
+                return null;
+            return value + " pixel" + (value == 1 ? "" : "s");
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PsdHeaderDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PsdHeaderDirectory.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PsdHeaderDirectory.java	(revision 15218)
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Holds the basic metadata found in the header of a Photoshop PSD file.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+@SuppressWarnings("WeakerAccess")
+public class PsdHeaderDirectory extends Directory
+{
+    /**
+     * The number of channels in the image, including any alpha channels. Supported range is 1 to 56.
+     */
+    public static final int TAG_CHANNEL_COUNT = 1;
+    /**
+     * The height of the image in pixels.
+     */
+    public static final int TAG_IMAGE_HEIGHT = 2;
+    /**
+     * The width of the image in pixels.
+     */
+    public static final int TAG_IMAGE_WIDTH = 3;
+    /**
+     * The number of bits per channel. Supported values are 1, 8, 16 and 32.
+     */
+    public static final int TAG_BITS_PER_CHANNEL = 4;
+    /**
+     * The color mode of the file. Supported values are:
+     * Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9.
+     */
+    public static final int TAG_COLOR_MODE = 5;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_CHANNEL_COUNT, "Channel Count");
+        _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_BITS_PER_CHANNEL, "Bits Per Channel");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+    }
+
+    public PsdHeaderDirectory()
+    {
+        this.setDescriptor(new PsdHeaderDescriptor(this));
+    }
+
+    @Override
+    @NotNull
+    public String getName()
+    {
+        return "PSD Header";
+    }
+
+    @Override
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/PsdReader.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/PsdReader.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/PsdReader.java	(revision 15218)
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2002-2019 Drew Noakes and contributors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    https://drewnoakes.com/code/exif/
+ *    https://github.com/drewnoakes/metadata-extractor
+ */
+
+package com.drew.metadata.photoshop;
+
+import com.drew.lang.SequentialReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Metadata;
+
+import java.io.IOException;
+
+/**
+ * Reads metadata stored within PSD file format data.
+ *
+ * @author Drew Noakes https://drewnoakes.com
+ */
+public class PsdReader
+{
+    public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata)
+    {
+        PsdHeaderDirectory directory = new PsdHeaderDirectory();
+        metadata.addDirectory(directory);
+
+        // FILE HEADER SECTION
+
+        try {
+            final int signature = reader.getInt32();
+            if (signature != 0x38425053) // "8BPS"
+            {
+                directory.addError("Invalid PSD file signature");
+                return;
+            }
+
+            final int version = reader.getUInt16();
+            if (version != 1 && version != 2)
+            {
+                directory.addError("Invalid PSD file version (must be 1 or 2)");
+                return;
+            }
+
+            // 6 reserved bytes are skipped here.  They should be zero.
+            reader.skip(6);
+
+            final int channelCount = reader.getUInt16();
+            directory.setInt(PsdHeaderDirectory.TAG_CHANNEL_COUNT, channelCount);
+
+            // even though this is probably an unsigned int, the max height in practice is 300,000
+            final int imageHeight = reader.getInt32();
+            directory.setInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT, imageHeight);
+
+            // even though this is probably an unsigned int, the max width in practice is 300,000
+            final int imageWidth = reader.getInt32();
+            directory.setInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH, imageWidth);
+
+            final int bitsPerChannel = reader.getUInt16();
+            directory.setInt(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL, bitsPerChannel);
+
+            final int colorMode = reader.getUInt16();
+            directory.setInt(PsdHeaderDirectory.TAG_COLOR_MODE, colorMode);
+        } catch (IOException e) {
+            directory.addError("Unable to read PSD header");
+            return;
+        }
+
+        // COLOR MODE DATA SECTION
+
+        try {
+            long sectionLength = reader.getUInt32();
+
+            /*
+             * Only indexed color and duotone (see the mode field in the File header section) have color mode data.
+             * For all other modes, this section is just the 4-byte length field, which is set to zero.
+             *
+             * Indexed color images: length is 768; color data contains the color table for the image,
+             *                       in non-interleaved order.
+             * Duotone images: color data contains the duotone specification (the format of which is not documented).
+             *                 Other applications that read Photoshop files can treat a duotone image as a gray	image,
+             *                 and just preserve the contents of the duotone information when reading and writing the
+             *                 file.
+             */
+
+            reader.skip(sectionLength);
+        } catch (IOException e) {
+            return;
+        }
+
+        // IMAGE RESOURCES SECTION
+
+        try {
+            long sectionLength = reader.getUInt32();
+
+            assert(sectionLength <= Integer.MAX_VALUE);
+
+            new PhotoshopReader().extract(reader, (int)sectionLength, metadata);
+        } catch (IOException e) {
+            // ignore
+        }
+
+        // LAYER AND MASK INFORMATION SECTION (skipped)
+
+        // IMAGE DATA SECTION (skipped)
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/Subpath.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/Subpath.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/Subpath.java	(revision 15218)
@@ -0,0 +1,58 @@
+package com.drew.metadata.photoshop;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a subpath created by Photoshop:
+ * <ul>
+ *   <li>Closed Bezier knot, linked</li>
+ *   <li>Closed Bezier knot, unlinked</li>
+ *   <li>Open Bezier knot, linked</li>
+ *   <li>Open Bezier knot, unlinked</li>
+ * </ul>
+ *
+ * @author Payton Garland
+ */
+public class Subpath
+{
+    private final ArrayList<Knot> _knots = new ArrayList<Knot>();
+    private final String _type;
+
+    public Subpath()
+    {
+        this("");
+    }
+
+    public Subpath(String type)
+    {
+        _type = type;
+    }
+
+    /**
+     * Appends a knot (set of 3 points) into the list
+     */
+    public void add(Knot knot)
+    {
+        _knots.add(knot);
+    }
+
+    /**
+     * Gets size of knots list
+     *
+     * @return size of knots ArrayList
+     */
+    public int size()
+    {
+        return _knots.size();
+    }
+
+    public Iterable<Knot> getKnots()
+    {
+        return _knots;
+    }
+
+    public String getType()
+    {
+        return _type;
+    }
+}
Index: trunk/src/com/drew/metadata/photoshop/package-info.java
===================================================================
--- trunk/src/com/drew/metadata/photoshop/package-info.java	(revision 15218)
+++ trunk/src/com/drew/metadata/photoshop/package-info.java	(revision 15218)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes for the extraction and modelling of Photoshop metadata.
+ */
+package com.drew.metadata.photoshop;
