diff --git a/imageio/imageio-clippath/license.txt b/imageio/imageio-clippath/license.txt
new file mode 100755
index 00000000..fe399516
--- /dev/null
+++ b/imageio/imageio-clippath/license.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2014, Harald Kuhr
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name "TwelveMonkeys" nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/imageio/imageio-clippath/pom.xml b/imageio/imageio-clippath/pom.xml
new file mode 100755
index 00000000..65e144c9
--- /dev/null
+++ b/imageio/imageio-clippath/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+
+ com.twelvemonkeys.imageio
+ imageio
+ 3.1-SNAPSHOT
+
+ imageio-clippath
+ TwelveMonkeys :: ImageIO :: Photoshop Path Support
+
+ Photoshop Clipping Path Support.
+
+
+
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+ tests
+
+
+ com.twelvemonkeys.imageio
+ imageio-metadata
+
+
+
diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathBuilder.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathBuilder.java
new file mode 100644
index 00000000..fa9faeea
--- /dev/null
+++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathBuilder.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.path;
+
+import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
+
+import javax.imageio.IIOException;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Path2D;
+import java.io.DataInput;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.twelvemonkeys.lang.Validate.isTrue;
+import static com.twelvemonkeys.lang.Validate.notNull;
+
+/**
+ * Creates a {@code Shape} object from an Adobe Photoshop Path resource.
+ *
+ * @see Adobe Photoshop Path resource format
+ * @author Jason Palmer, itemMaster LLC
+ * @author Harald Kuhr
+ */
+public final class AdobePathBuilder {
+ final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.path.debug"));
+
+ private final DataInput data;
+
+ /**
+ * Creates a path builder that will read its data from a {@code DataInput}, such as an
+ * {@code ImageInputStream}.
+ * The data length is assumed to be a multiple of 26.
+ *
+ * @param data the input to read data from.
+ * @throws java.lang.IllegalArgumentException if {@code data} is {@code null}
+ */
+ public AdobePathBuilder(final DataInput data) {
+ notNull(data, "data");
+ this.data = data;
+ }
+
+ /**
+ * Creates a path builder that will read its data from a {@code byte} array.
+ * The array length must be a multiple of 26, and greater than 0.
+ *
+ * @param data the array to read data from.
+ * @throws java.lang.IllegalArgumentException if {@code data} is {@code null}, or not a multiple of 26.
+ */
+ public AdobePathBuilder(final byte[] data) {
+ this(new ByteArrayImageInputStream(
+ notNull(data, "data"), 0,
+ isTrue(data.length > 0 && data.length % 26 == 0, data.length, "data.length must be a multiple of 26: %d")
+ ));
+ }
+
+ /**
+ * Builds the path.
+ *
+ * @return the path
+ * @throws javax.imageio.IIOException if the input contains a bad path data.
+ * @throws IOException if a general I/O exception occurs during reading.
+ */
+ public Path2D path() throws IOException {
+ List> subPaths = new ArrayList>();
+ List currentPath = null;
+ int currentPathLength = 0;
+
+ AdobePathSegment segment;
+ while ((segment = nextSegment()) != null) {
+
+ if (DEBUG) {
+ System.out.println(segment);
+ }
+
+ if (segment.selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD || segment.selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD) {
+ if (currentPath != null) {
+ if (currentPathLength != currentPath.size()) {
+ throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
+ }
+ subPaths.add(currentPath);
+ }
+
+ currentPath = new ArrayList(segment.length);
+ currentPathLength = segment.length;
+ }
+ else if (segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED
+ || segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED
+ || segment.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED
+ || segment.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED) {
+ if (currentPath == null) {
+ throw new IIOException("Bad path, missing subpath length record");
+ }
+ if (currentPath.size() >= currentPathLength) {
+ throw new IIOException(String.format("Bad path, expected %d segments, found%d", currentPathLength, currentPath.size()));
+ }
+
+ currentPath.add(segment);
+ }
+ }
+
+ // now add the last one
+ if (currentPath != null) {
+ if (currentPathLength != currentPath.size()) {
+ throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
+ }
+
+ subPaths.add(currentPath);
+ }
+
+ // now we have collected the PathPoints now create a Shape.
+ return pathToShape(subPaths);
+ }
+
+ /**
+ * The Correct Order... P1, P2, P3, P4, P5, P6 (Closed) moveTo(P1)
+ * curveTo(P1.cpl, P2.cpp, P2.ap); curveTo(P2.cpl, P3.cppy, P3.ap);
+ * curveTo(P3.cpl, P4.cpp, P4.ap); curveTo(P4.cpl, P5.cpp, P5.ap);
+ * curveTo(P5.cply, P6.cpp, P6.ap); curveTo(P6.cpl, P1.cpp, P1.ap);
+ * closePath()
+ */
+ private Path2D pathToShape(final List> paths) {
+ GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD, paths.size());
+ GeneralPath subpath = null;
+
+ for (List points : paths) {
+ int length = points.size();
+
+ for (int i = 0; i < points.size(); i++) {
+ AdobePathSegment current = points.get(i);
+
+ int step = i == 0 ? 0 : i == length - 1 ? 2 : 1;
+
+ switch (step) {
+ // begin
+ case 0: {
+ subpath = new GeneralPath(Path2D.WIND_EVEN_ODD, length);
+ subpath.moveTo(current.apx, current.apy);
+
+ if (length > 1) {
+ AdobePathSegment next = points.get((i + 1));
+ subpath.curveTo(current.cplx, current.cply, next.cppx, next.cppy, next.apx, next.apy);
+ }
+ else {
+ subpath.lineTo(current.apx, current.apy);
+ }
+
+ break;
+ }
+ // middle
+ case 1: {
+ AdobePathSegment next = points.get((i + 1)); // we are always guaranteed one more.
+ subpath.curveTo(current.cplx, current.cply, next.cppx, next.cppy, next.apx, next.apy);
+
+ break;
+ }
+ // end
+ case 2: {
+ AdobePathSegment first = points.get(0);
+
+ if (first.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED || first.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED) {
+ subpath.curveTo(current.cplx, current.cply, first.cppx, first.cppy, first.apx, first.apy);
+ subpath.closePath();
+ path.append(subpath, false);
+ }
+ else {
+ subpath.lineTo(current.apx, current.apy);
+ path.append(subpath, true);
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ return path;
+ }
+
+ private AdobePathSegment nextSegment() throws IOException {
+ // Each segment is 26 bytes
+ int selector;
+ try {
+ selector = data.readUnsignedShort();
+ }
+ catch (EOFException eof) {
+ // No more data, we're done
+ return null;
+ }
+
+ // Spec says Fill rule is ignored by Photoshop... Probably not.. ;-)
+ // TODO: Replace with switch + handle all types!
+ // TODO: ...or Move logic to AdobePathSegment?
+ if (selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD || selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD) {
+ int size = data.readUnsignedShort();
+ // data.position(data.position() + 22); // Skip remaining
+ data.skipBytes(22);
+ return new AdobePathSegment(selector, size);
+ }
+
+ return new AdobePathSegment(
+ selector,
+ readFixedPoint(data.readInt()),
+ readFixedPoint(data.readInt()),
+ readFixedPoint(data.readInt()),
+ readFixedPoint(data.readInt()),
+ readFixedPoint(data.readInt()),
+ readFixedPoint(data.readInt())
+ );
+ }
+
+ private static double readFixedPoint(final int fixed) {
+ return ((double) fixed / 0x1000000);
+ }
+}
diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathSegment.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathSegment.java
new file mode 100644
index 00000000..c8d91449
--- /dev/null
+++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathSegment.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.path;
+
+import com.twelvemonkeys.lang.Validate;
+
+/**
+ * Adobe path segment.
+ *
+ * @see Adobe Photoshop Path resource format
+ * @author Jason Palmer, itemMaster LLC
+ * @author Harald Kuhr
+*/
+final class AdobePathSegment {
+ public final static int CLOSED_SUBPATH_LENGTH_RECORD = 0;
+ public final static int CLOSED_SUBPATH_BEZIER_LINKED = 1;
+ public final static int CLOSED_SUBPATH_BEZIER_UNLINKED = 2;
+ public final static int OPEN_SUBPATH_LENGTH_RECORD = 3;
+ public final static int OPEN_SUBPATH_BEZIER_LINKED = 4;
+ public final static int OPEN_SUBPATH_BEZIER_UNLINKED = 5;
+ public final static int PATH_FILL_RULE_RECORD = 6;
+ public final static int CLIPBOARD_RECORD = 7;
+ public final static int INITIAL_FILL_RULE_RECORD = 8;
+
+ public final static String[] SELECTOR_NAMES = {
+ "Closed subpath length record",
+ "Closed subpath Bezier knot, linked",
+ "Closed subpath Bezier knot, unlinked",
+ "Open subpath length record",
+ "Open subpath Bezier knot, linked",
+ "Open subpath Bezier knot, unlinked",
+ "Path fill rule record",
+ "Clipboard record",
+ "Initial fill rule record"
+ };
+
+ final int selector;
+ final int length;
+
+ final double cppy;
+ final double cppx;
+ final double apy;
+ final double apx;
+ final double cply;
+ final double cplx;
+
+ AdobePathSegment(final int selector,
+ final double cppy, final double cppx,
+ final double apy, final double apx,
+ final double cply, final double cplx) {
+ this(selector, -1, cppy, cppx, apy, apx, cply, cplx);
+ }
+
+ AdobePathSegment(final int selector, final int length) {
+ this(selector, length, -1, -1, -1, -1, -1, -1);
+ }
+
+ private AdobePathSegment(final int selector, final int length,
+ final double cppy, final double cppx,
+ final double apy, final double apx,
+ final double cply, final double cplx) {
+ // Validate selector, size and points
+ switch (selector) {
+ case CLOSED_SUBPATH_LENGTH_RECORD:
+ case OPEN_SUBPATH_LENGTH_RECORD:
+ Validate.isTrue(length >= 0, length, "Bad size: %d");
+ break;
+ case CLOSED_SUBPATH_BEZIER_LINKED:
+ case CLOSED_SUBPATH_BEZIER_UNLINKED:
+ case OPEN_SUBPATH_BEZIER_LINKED:
+ case OPEN_SUBPATH_BEZIER_UNLINKED:
+ Validate.isTrue(
+ cppx >= 0 && cppx <= 1 && cppy >= 0 && cppy <= 1,
+ String.format("Unexpected point: [%f, %f]", cppx ,cppy)
+ );
+ break;
+ case PATH_FILL_RULE_RECORD:
+ case CLIPBOARD_RECORD:
+ case INITIAL_FILL_RULE_RECORD:
+ break;
+ default:
+ throw new IllegalArgumentException("Bad selector: " + selector);
+ }
+
+ this.selector = selector;
+ this.length = length;
+ this.cppy = cppy;
+ this.cppx = cppx;
+ this.apy = apy;
+ this.apx = apx;
+ this.cply = cply;
+ this.cplx = cplx;
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+
+ AdobePathSegment that = (AdobePathSegment) other;
+
+ return Double.compare(that.apx, apx) == 0
+ && Double.compare(that.apy, apy) == 0
+ && Double.compare(that.cplx, cplx) == 0
+ && Double.compare(that.cply, cply) == 0
+ && Double.compare(that.cppx, cppx) == 0
+ && Double.compare(that.cppy, cppy) == 0
+ && selector == that.selector
+ && length == that.length;
+
+ }
+
+ @Override
+ public int hashCode() {
+ long tempBits;
+
+ int result = selector;
+ result = 31 * result + length;
+ tempBits = Double.doubleToLongBits(cppy);
+ result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
+ tempBits = Double.doubleToLongBits(cppx);
+ result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
+ tempBits = Double.doubleToLongBits(apy);
+ result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
+ tempBits = Double.doubleToLongBits(apx);
+ result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
+ tempBits = Double.doubleToLongBits(cply);
+ result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
+ tempBits = Double.doubleToLongBits(cplx);
+ result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ switch (selector) {
+ case INITIAL_FILL_RULE_RECORD:
+ case PATH_FILL_RULE_RECORD:
+ return String.format("Rule(selector=%s, rule=%d)", SELECTOR_NAMES[selector], length);
+ case CLOSED_SUBPATH_LENGTH_RECORD:
+ case OPEN_SUBPATH_LENGTH_RECORD:
+ return String.format("Len(selector=%s, totalPoints=%d)", SELECTOR_NAMES[selector], length);
+ default:
+ // fall-through
+ }
+ return String.format("Pt(preX=%.3f, preY=%.3f, knotX=%.3f, knotY=%.3f, postX=%.3f, postY=%.3f, selector=%s)", cppx, cppy, apx, apy, cplx, cply, SELECTOR_NAMES[selector]);
+ }
+}
diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
new file mode 100644
index 00000000..1c3c5fd4
--- /dev/null
+++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.path;
+
+import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
+import com.twelvemonkeys.imageio.metadata.Directory;
+import com.twelvemonkeys.imageio.metadata.Entry;
+import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
+import com.twelvemonkeys.imageio.metadata.exif.TIFF;
+import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
+import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
+import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
+import com.twelvemonkeys.imageio.metadata.psd.PSD;
+import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
+import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
+import com.twelvemonkeys.imageio.stream.SubImageInputStream;
+
+import javax.imageio.ImageIO;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.MemoryCacheImageInputStream;
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Path2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.twelvemonkeys.lang.Validate.isTrue;
+import static com.twelvemonkeys.lang.Validate.notNull;
+
+/**
+ * Support for various Adobe Photoshop Path related operations:
+ *
+ * - Extract a path from an image input stream, {@link #readPath}
+ * - Apply a given path to a given {@code BufferedImage} {@link #applyClippingPath}
+ * - Read an image with path applied {@link #readClipped}
+ *
+ *
+ * @see Adobe Photoshop Path resource format
+ * @author Jason Palmer, itemMaster LLC
+ * @author Harald Kuhr
+ * @author last modified by $Author: harald.kuhr$
+ * @version $Id: Paths.java,v 1.0 08/12/14 harald.kuhr Exp$
+ */
+public final class Paths {
+
+ private Paths() {}
+
+ /**
+ * Reads the clipping path from the given input stream, if any.
+ * Supports PSD, JPEG and TIFF as container formats for Photoshop resources,
+ * or a "bare" PSD Image Resource Block.
+ *
+ * @param stream the input stream to read from, not {@code null}.
+ * @return the path, or {@code null} if no path is found
+ * @throws IOException if a general I/O exception occurs during reading.
+ * @throws javax.imageio.IIOException if the input contains a bad path data.
+ * @throws java.lang.IllegalArgumentException is {@code stream} is {@code null}.
+ *
+ * @see com.twelvemonkeys.imageio.path.AdobePathBuilder
+ */
+ public static Path2D readPath(final ImageInputStream stream) throws IOException {
+ notNull(stream, "stream");
+
+ int magic = readMagic(stream);
+
+ if (magic == PSD.RESOURCE_TYPE) {
+ // This is a PSD Image Resource BLock, we can parse directly
+ return buildPathFromPhotoshopResources(stream);
+ }
+ else if (magic == PSD.SIGNATURE_8BPS) {
+ // PSD version
+ // 4 byte magic, 2 byte version, 6 bytes reserved, 2 byte channels,
+ // 4 byte height, 4 byte width, 2 byte bit depth, 2 byte mode
+ stream.skipBytes(26);
+
+ // 4 byte color mode data length + n byte color mode data
+ long colorModeLen = stream.readUnsignedInt();
+ stream.skipBytes(colorModeLen);
+
+ // 4 byte image resources length
+ long imageResourcesLen = stream.readUnsignedInt();
+
+ // Image resources
+ return buildPathFromPhotoshopResources(new SubImageInputStream(stream, imageResourcesLen));
+ }
+ else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) {
+ // JPEG version
+ Map> segmentIdentifiers = new LinkedHashMap>();
+ segmentIdentifiers.put(JPEG.APP13, Arrays.asList("Photoshop 3.0"));
+
+ List photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
+
+ if (!photoshop.isEmpty()) {
+ return buildPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data()));
+ }
+ }
+ else if (magic >>> 16 == TIFF.BYTE_ORDER_MARK_BIG_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC
+ || magic >>> 16 == TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC << 8) {
+ // TIFF version
+ CompoundDirectory IFDs = (CompoundDirectory) new EXIFReader().read(stream);
+
+ Directory directory = IFDs.getDirectory(0);
+ Entry photoshop = directory.getEntryById(TIFF.TAG_PHOTOSHOP);
+
+ if (photoshop != null) {
+ return buildPathFromPhotoshopResources(new ByteArrayImageInputStream((byte[]) photoshop.getValue()));
+ }
+ }
+
+ // Unknown file format, or no path found
+ return null;
+ }
+
+ private static int readMagic(final ImageInputStream stream) throws IOException {
+ stream.mark();
+
+ try {
+ return stream.readInt();
+ }
+ finally {
+ stream.reset();
+ }
+ }
+
+ private static Path2D buildPathFromPhotoshopResources(final ImageInputStream stream) throws IOException {
+ Directory resourceBlocks = new PSDReader().read(stream);
+
+ if (AdobePathBuilder.DEBUG) {
+ System.out.println("resourceBlocks: " + resourceBlocks);
+ }
+
+ Entry resourceBlock = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH);
+
+ if (resourceBlock != null) {
+ return new AdobePathBuilder((byte[]) resourceBlock.getValue()).path();
+ }
+
+ return null;
+ }
+
+ /**
+ * Applies the clipping path to the given image.
+ * All pixels outside the path will be transparent.
+ *
+ * @param clip the clipping path, not {@code null}
+ * @param image the image to clip, not {@code null}
+ * @return the clipped image.
+ *
+ * @throws java.lang.IllegalArgumentException if {@code clip} or {@code image} is {@code null}.
+ */
+ public static BufferedImage applyClippingPath(final Shape clip, final BufferedImage image) {
+ return applyClippingPath(clip, notNull(image, "image"), new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));
+ }
+
+ /**
+ * Applies the clipping path to the given image.
+ * Client code may decide the type of the {@code destination} image.
+ * The {@code destination} image is assumed to be fully transparent,
+ * and have same dimensions as {@code image}.
+ * All pixels outside the path will be transparent.
+ *
+ * @param clip the clipping path, not {@code null}.
+ * @param image the image to clip, not {@code null}.
+ * @param destination the destination image, may not be {@code null} or same instance as {@code image}.
+ * @return the clipped image.
+ *
+ * @throws java.lang.IllegalArgumentException if {@code clip}, {@code image} or {@code destination} is {@code null},
+ * or if {@code destination} is the same instance as {@code image}.
+ */
+ public static BufferedImage applyClippingPath(final Shape clip, final BufferedImage image, final BufferedImage destination) {
+ notNull(clip, "clip");
+ notNull(image, "image");
+ isTrue(destination != null && destination != image, "destination may not be null or same instance as image");
+
+ Graphics2D g = destination.createGraphics();
+
+ try {
+ AffineTransform originalTransform = g.getTransform();
+
+ // Fill the clip shape, with antialias, scaled up to the image's size
+ g.scale(image.getWidth(), image.getHeight());
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.fill(clip);
+
+ // Draw the image inside the clip shape
+ g.setTransform(originalTransform);
+ g.setComposite(AlphaComposite.SrcIn);
+ g.drawImage(image, 0, 0, null);
+ }
+ finally {
+ g.dispose();
+ }
+
+ return destination;
+ }
+
+ /**
+ * Reads the clipping path from the given input stream, if any,
+ * and applies it to the first image in the stream.
+ * If no path was found, the image is returned without any clipping.
+ * Supports PSD, JPEG and TIFF as container formats for Photoshop resources.
+ *
+ * @param stream the stream to read from, not {@code null}
+ * @return the clipped image
+ *
+ * @throws IOException if a general I/O exception occurs during reading.
+ * @throws javax.imageio.IIOException if the input contains a bad image or path data.
+ * @throws java.lang.IllegalArgumentException is {@code stream} is {@code null}.
+ */
+ public static BufferedImage readClipped(final ImageInputStream stream) throws IOException {
+ Shape clip = readPath(stream);
+
+ stream.seek(0);
+ BufferedImage image = ImageIO.read(stream);
+
+ if (clip == null) {
+ return image;
+ }
+
+ return applyClippingPath(clip, image);
+ }
+
+ // Test code
+ public static void main(final String[] args) throws IOException, InterruptedException {
+ BufferedImage destination = readClipped(ImageIO.createImageInputStream(new File(args[0])));
+
+ File tempFile = File.createTempFile("clipped-", ".png");
+ tempFile.deleteOnExit();
+ ImageIO.write(destination, "PNG", tempFile);
+
+ Desktop.getDesktop().open(tempFile);
+
+ Thread.sleep(3000l);
+
+ if (!tempFile.delete()) {
+ System.err.printf("%s not deleted\n", tempFile);
+ }
+ }
+
+}
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java
new file mode 100644
index 00000000..5ae16d25
--- /dev/null
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java
@@ -0,0 +1,134 @@
+package com.twelvemonkeys.imageio.path;
+
+import org.junit.Test;
+
+import javax.imageio.IIOException;
+import javax.imageio.stream.ImageInputStream;
+import java.awt.geom.Path2D;
+import java.io.DataInput;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals;
+import static com.twelvemonkeys.imageio.path.PathsTest.readExpectedPath;
+import static org.junit.Assert.assertNotNull;
+
+public class AdobePathBuilderTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateNullBytes() {
+ new AdobePathBuilder((byte[]) null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateNull() {
+ new AdobePathBuilder((DataInput) null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateEmpty() {
+ new AdobePathBuilder(new byte[0]);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateShortPath() {
+ new AdobePathBuilder(new byte[3]);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateImpossiblePath() {
+ new AdobePathBuilder(new byte[7]);
+ }
+
+ @Test
+ public void testCreate() {
+ new AdobePathBuilder(new byte[52]);
+ }
+
+ @Test
+ public void testNoPath() throws IOException {
+ Path2D path = new AdobePathBuilder(new byte[26]).path();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testShortPath() throws IOException {
+ byte[] data = new byte[26];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD);
+ buffer.putShort((short) 1);
+
+ Path2D path = new AdobePathBuilder(data).path();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testShortPathToo() throws IOException {
+ byte[] data = new byte[52];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD);
+ buffer.putShort((short) 2);
+ buffer.position(buffer.position() + 22);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+
+ Path2D path = new AdobePathBuilder(data).path();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testLongPath() throws IOException {
+ byte[] data = new byte[78];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD);
+ buffer.putShort((short) 1);
+ buffer.position(buffer.position() + 22);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+ buffer.position(buffer.position() + 24);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+
+ Path2D path = new AdobePathBuilder(data).path();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testPathMissingLength() throws IOException {
+ byte[] data = new byte[26];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+
+ Path2D path = new AdobePathBuilder(data).path();
+ assertNotNull(path);
+ }
+
+ @Test
+ public void testSimplePath() throws IOException {
+ // We'll read this from a real file, with hardcoded offsets for simplicity
+ // PSD IRB: offset: 34, length: 32598
+ // Clipping path: offset: 31146, length: 1248
+ ImageInputStream stream = PathsTest.resourceAsIIOStream("/psd/grape_with_path.psd");
+ stream.seek(34 + 31146);
+ byte[] data = new byte[1248];
+ stream.readFully(data);
+
+ Path2D path = new AdobePathBuilder(data).path();
+
+ assertNotNull(path);
+ assertPathEquals(path, readExpectedPath("/ser/grape-path.ser"));
+ }
+
+ @Test
+ public void testComplexPath() throws IOException {
+ // We'll read this from a real file, with hardcoded offsets for simplicity
+ // PSD IRB: offset: 16970, length: 11152
+ // Clipping path: offset: 9250, length: 1534
+ ImageInputStream stream = PathsTest.resourceAsIIOStream("/tiff/big-endian-multiple-clips.tif");
+ stream.seek(16970 + 9250);
+ byte[] data = new byte[1534];
+ stream.readFully(data);
+
+ Path2D path = new AdobePathBuilder(data).path();
+
+ assertNotNull(path);
+ assertPathEquals(path, readExpectedPath("/ser/multiple-clips.ser"));
+ }
+}
\ No newline at end of file
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java
new file mode 100644
index 00000000..fbdda65b
--- /dev/null
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java
@@ -0,0 +1,229 @@
+package com.twelvemonkeys.imageio.path;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * AdobePathSegmentTest.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: harald.kuhr$
+ * @version $Id: AdobePathSegmentTest.java,v 1.0 13/12/14 harald.kuhr Exp$
+ */
+public class AdobePathSegmentTest {
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateBadSelectorNegative() {
+ new AdobePathSegment(-1, 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateBadSelector() {
+ new AdobePathSegment(9, 2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateOpenLengthRecordNegative() {
+ new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, -1);
+
+ }
+
+ @Test
+ public void testCreateOpenLengthRecord() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42);
+
+ assertEquals(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, segment.selector);
+ assertEquals(42, segment.length);
+ assertEquals(-1, segment.cppx, 0);
+ assertEquals(-1, segment.cppy, 0);
+ assertEquals(-1, segment.apx, 0);
+ assertEquals(-1, segment.apy, 0);
+ assertEquals(-1, segment.cplx, 0);
+ assertEquals(-1, segment.cply, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateClosedLengthRecordNegative() {
+ new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, -42);
+ }
+
+ @Test
+ public void testCreateClosedLengthRecord() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 27);
+
+ assertEquals(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, segment.selector);
+ assertEquals(27, segment.length);
+ assertEquals(-1, segment.cppx, 0);
+ assertEquals(-1, segment.cppy, 0);
+ assertEquals(-1, segment.apx, 0);
+ assertEquals(-1, segment.apy, 0);
+ assertEquals(-1, segment.cplx, 0);
+ assertEquals(-1, segment.cply, 0);
+ }
+
+ /// Open subpath
+
+ @Test
+ public void testCreateOpenLinkedRecord() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
+
+ assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, segment.selector);
+ assertEquals(-1, segment.length);
+ assertEquals(.5, segment.cppx, 0);
+ assertEquals(.5, segment.cppy, 0);
+ assertEquals(0, segment.apx, 0);
+ assertEquals(0, segment.apy, 0);
+ assertEquals(1, segment.cplx, 0);
+ assertEquals(1, segment.cply, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateOpenLinkedRecordBad() {
+ new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, 44);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateOpenLinkedRecordNegative() {
+ new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, -.5, -.5, 0, 0, 1, 1);
+ }
+
+ @Test
+ public void testCreateOpenUnlinkedRecord() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
+
+ assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, segment.selector);
+ assertEquals(-1, segment.length);
+ assertEquals(.5, segment.cppx, 0);
+ assertEquals(.5, segment.cppy, 0);
+ assertEquals(0, segment.apx, 0);
+ assertEquals(0, segment.apy, 0);
+ assertEquals(1, segment.cplx, 0);
+ assertEquals(1, segment.cply, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateOpenUnlinkedRecordBad() {
+ new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, 44);
+ }
+
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateOpenUnlinkedRecordNegative() {
+ new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, -.5, -.5, 0, 0, 1, 1);
+ }
+
+ /// Closed subpath
+
+ @Test
+ public void testCreateClosedLinkedRecord() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
+
+ assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, segment.selector);
+ assertEquals(-1, segment.length);
+ assertEquals(.5, segment.cppx, 0);
+ assertEquals(.5, segment.cppy, 0);
+ assertEquals(0, segment.apx, 0);
+ assertEquals(0, segment.apy, 0);
+ assertEquals(1, segment.cplx, 0);
+ assertEquals(1, segment.cply, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateClosedLinkedRecordBad() {
+ new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 44);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateClosedLinkedRecordNegative() {
+ new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, -.5, -.5, 0, 0, 1, 1);
+ }
+
+ @Test
+ public void testCreateClosedUnlinkedRecord() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
+
+ assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, segment.selector);
+ assertEquals(-1, segment.length);
+ assertEquals(.5, segment.cppx, 0);
+ assertEquals(.5, segment.cppy, 0);
+ assertEquals(0, segment.apx, 0);
+ assertEquals(0, segment.apy, 0);
+ assertEquals(1, segment.cplx, 0);
+ assertEquals(1, segment.cply, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateClosedUnlinkedRecordBad() {
+ new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, 44);
+ }
+
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateClosedUnlinkedRecordNegative() {
+ new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, -.5, -.5, 0, 0, 1, 1);
+ }
+
+ @Test
+ public void testToStringRule() {
+ String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD, 2).toString();
+ assertTrue(string, string.startsWith("Rule"));
+ assertTrue(string, string.contains("Initial"));
+ assertTrue(string, string.contains("fill"));
+ assertTrue(string, string.contains("rule=2"));
+ }
+
+ @Test
+ public void testToStringLength() {
+ String string = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2).toString();
+ assertTrue(string, string.startsWith("Len"));
+ assertTrue(string, string.contains("Closed"));
+ assertTrue(string, string.contains("subpath"));
+ assertTrue(string, string.contains("totalPoints=2"));
+
+ string = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42).toString();
+ assertTrue(string, string.startsWith("Len"));
+ assertTrue(string, string.contains("Open"));
+ assertTrue(string, string.contains("subpath"));
+ assertTrue(string, string.contains("totalPoints=42"));
+ }
+
+ @Test
+ public void testToStringOther() {
+ String string = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0).toString();
+ assertTrue(string, string.startsWith("Pt"));
+ assertTrue(string, string.contains("Open"));
+ assertTrue(string, string.contains("Bezier"));
+ assertTrue(string, string.contains("linked"));
+
+ string = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0).toString();
+ assertTrue(string, string.startsWith("Pt"));
+ assertTrue(string, string.contains("Closed"));
+ assertTrue(string, string.contains("Bezier"));
+ assertTrue(string, string.contains("linked"));
+ }
+
+ @Test
+ public void testEqualsLength() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2);
+ assertEquals(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2), segment);
+ assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 3).equals(segment));
+ assertFalse(new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 2).equals(segment));
+ }
+
+ @Test
+ public void testEqualsOther() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0);
+ assertEquals(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0), segment);
+ assertFalse(new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0).equals(segment));
+ assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 0, 1, 1, 0, 0).equals(segment));
+ assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0.1, 1, 1, 0, 0).equals(segment));
+ }
+
+ @Test
+ public void testHashCodeLength() {
+ AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2);
+ assertEquals(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2).hashCode(), segment.hashCode());
+ assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 3).hashCode() == segment.hashCode());
+ assertFalse(new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 2).hashCode() == segment.hashCode());
+ }
+}
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java
new file mode 100644
index 00000000..c7115f67
--- /dev/null
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java
@@ -0,0 +1,233 @@
+package com.twelvemonkeys.imageio.path;
+
+import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
+import com.twelvemonkeys.imageio.stream.SubImageInputStream;
+import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
+import org.junit.Test;
+
+import javax.imageio.ImageIO;
+import javax.imageio.spi.IIORegistry;
+import javax.imageio.stream.ImageInputStream;
+import java.awt.*;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+
+import static org.junit.Assert.*;
+
+/**
+ * PathsTest.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: harald.kuhr$
+ * @version $Id: PathsTest.java,v 1.0 12/12/14 harald.kuhr Exp$
+ */
+public class PathsTest {
+ static {
+ IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadPathNull() throws IOException {
+ Paths.readPath(null);
+ }
+
+ @Test
+ public void testReadPathUnknown() throws IOException {
+ assertNull(Paths.readPath(new ByteArrayImageInputStream(new byte[42])));
+ }
+
+ @Test
+ public void testGrapeJPEG() throws IOException {
+ ImageInputStream stream = resourceAsIIOStream("/jpeg/grape_with_path.jpg");
+
+ Path2D path = Paths.readPath(stream);
+
+ assertNotNull(path);
+ assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
+ }
+
+ @Test
+ public void testGrapePSD() throws IOException {
+ ImageInputStream stream = resourceAsIIOStream("/psd/grape_with_path.psd");
+
+ Path2D path = Paths.readPath(stream);
+
+ assertNotNull(path);
+ assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
+
+ }
+
+ @Test
+ public void testGrapeTIFF() throws IOException {
+ ImageInputStream stream = resourceAsIIOStream("/tiff/little-endian-grape_with_path.tif");
+
+ Path2D path = Paths.readPath(stream);
+
+ assertNotNull(path);
+ assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
+ }
+
+ @Test
+ public void testMultipleTIFF() throws IOException {
+ ImageInputStream stream = resourceAsIIOStream("/tiff/big-endian-multiple-clips.tif");
+
+ Shape path = Paths.readPath(stream);
+
+ assertNotNull(path);
+ }
+
+ @Test
+ public void testGrape8BIM() throws IOException {
+ ImageInputStream stream = resourceAsIIOStream("/psd/grape_with_path.psd");
+
+ // PSD image resources from position 34, length 32598
+ stream.seek(34);
+ stream = new SubImageInputStream(stream, 32598);
+
+ Path2D path = Paths.readPath(stream);
+
+ assertNotNull(path);
+ assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testApplyClippingPathNullPath() throws IOException {
+ Paths.applyClippingPath(null, new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testApplyClippingPathNullSource() throws IOException {
+ Paths.applyClippingPath(new GeneralPath(), null);
+ }
+
+ @Test
+ public void testApplyClippingPath() throws IOException {
+ BufferedImage source = new BufferedImage(20, 20, BufferedImage.TYPE_3BYTE_BGR);
+
+ Path2D path = readExpectedPath("/ser/grape-path.ser");
+
+ BufferedImage image = Paths.applyClippingPath(path, source);
+
+ assertNotNull(image);
+ // Same dimensions as original
+ assertEquals(source.getWidth(), image.getWidth());
+ assertEquals(source.getHeight(), image.getHeight());
+ // Transparent
+ assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
+
+ // Corners (at least) should be transparent
+ assertEquals(0, image.getRGB(0, 0));
+ assertEquals(0, image.getRGB(source.getWidth() - 1, 0));
+ assertEquals(0, image.getRGB(0, source.getHeight() - 1));
+ assertEquals(0, image.getRGB(source.getWidth() - 1, source.getHeight() - 1));
+
+ // Center opaque
+ assertEquals(0xff, image.getRGB(source.getWidth() / 2, source.getHeight() / 2) >>> 24);
+
+ // TODO: Mor sophisticated test that tests all pixels outside path...
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testApplyClippingPathNullDestination() throws IOException {
+ Paths.applyClippingPath(new GeneralPath(), new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), null);
+ }
+
+ @Test
+ public void testApplyClippingPathCustomDestination() throws IOException {
+ BufferedImage source = new BufferedImage(20, 20, BufferedImage.TYPE_3BYTE_BGR);
+
+ Path2D path = readExpectedPath("/ser/grape-path.ser");
+
+ // Destination is intentionally larger than source
+ BufferedImage destination = new BufferedImage(30, 30, BufferedImage.TYPE_4BYTE_ABGR);
+ BufferedImage image = Paths.applyClippingPath(path, source, destination);
+
+ assertSame(destination, image);
+
+ // Corners (at least) should be transparent
+ assertEquals(0, image.getRGB(0, 0));
+ assertEquals(0, image.getRGB(image.getWidth() - 1, 0));
+ assertEquals(0, image.getRGB(0, image.getHeight() - 1));
+ assertEquals(0, image.getRGB(image.getWidth() - 1, image.getHeight() - 1));
+
+ // "inner" corners
+ assertEquals(0, image.getRGB(source.getWidth() - 1, 0));
+ assertEquals(0, image.getRGB(0, source.getHeight() - 1));
+ assertEquals(0, image.getRGB(source.getWidth() - 1, source.getHeight() - 1));
+
+ // Center opaque
+ assertEquals(0xff, image.getRGB(source.getWidth() / 2, source.getHeight() / 2) >>> 24);
+
+ // TODO: Mor sophisticated test that tests all pixels outside path...
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testReadClippedNull() throws IOException {
+ Paths.readClipped(null);
+ }
+
+ @Test
+ public void testReadClipped() throws IOException {
+ BufferedImage image = Paths.readClipped(resourceAsIIOStream("/jpeg/grape_with_path.jpg"));
+
+ assertNotNull(image);
+ // Same dimensions as original
+ assertEquals(857, image.getWidth());
+ assertEquals(1800, image.getHeight());
+ // Transparent
+ assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
+
+ // Corners (at least) should be transparent
+ assertEquals(0, image.getRGB(0, 0));
+ assertEquals(0, image.getRGB(image.getWidth() - 1, 0));
+ assertEquals(0, image.getRGB(0, image.getHeight() - 1));
+ assertEquals(0, image.getRGB(image.getWidth() - 1, image.getHeight() - 1));
+
+ // Center opaque
+ assertEquals(0xff, image.getRGB(image.getWidth() / 2, image.getHeight() / 2) >>> 24);
+
+ // TODO: Mor sophisticated test that tests all pixels outside path...
+ }
+
+ // TODO: Test read image without path, as no-op
+
+ static ImageInputStream resourceAsIIOStream(String name) throws IOException {
+ return ImageIO.createImageInputStream(PathsTest.class.getResource(name));
+ }
+
+ static Path2D readExpectedPath(final String resource) throws IOException {
+ ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource));
+
+ try {
+ return (Path2D) ois.readObject();
+ }
+ catch (ClassNotFoundException e) {
+ throw new IOException(e);
+ }
+ finally {
+ ois.close();
+ }
+ }
+
+ static void assertPathEquals(final Path2D expectedPath, final Path2D actualPath) {
+ PathIterator expectedIterator = expectedPath.getPathIterator(null);
+ PathIterator actualIterator = actualPath.getPathIterator(null);
+ float[] expectedCoords = new float[6];
+ float[] actualCoords = new float[6];
+
+ while(!actualIterator.isDone()) {
+ int expectedType = expectedIterator.currentSegment(expectedCoords);
+ int actualType = actualIterator.currentSegment(actualCoords);
+
+ assertEquals(expectedType, actualType);
+ assertArrayEquals(expectedCoords, actualCoords, 0);
+
+ actualIterator.next();
+ expectedIterator.next();
+ }
+ }
+}
diff --git a/imageio/imageio-clippath/src/test/resources/images.txt b/imageio/imageio-clippath/src/test/resources/images.txt
new file mode 100644
index 00000000..433c82e4
--- /dev/null
+++ b/imageio/imageio-clippath/src/test/resources/images.txt
@@ -0,0 +1 @@
+Sample images kindly provided by itemMaster LLC (https://www.itemmaster.com/).
\ No newline at end of file
diff --git a/imageio/imageio-clippath/src/test/resources/jpeg/grape_with_path.jpg b/imageio/imageio-clippath/src/test/resources/jpeg/grape_with_path.jpg
new file mode 100644
index 00000000..bd0befd9
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/jpeg/grape_with_path.jpg differ
diff --git a/imageio/imageio-clippath/src/test/resources/jpeg/single-clip.jpg b/imageio/imageio-clippath/src/test/resources/jpeg/single-clip.jpg
new file mode 100644
index 00000000..e8dc78f5
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/jpeg/single-clip.jpg differ
diff --git a/imageio/imageio-clippath/src/test/resources/psd/grape_with_path.psd b/imageio/imageio-clippath/src/test/resources/psd/grape_with_path.psd
new file mode 100644
index 00000000..6a7728a4
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/psd/grape_with_path.psd differ
diff --git a/imageio/imageio-clippath/src/test/resources/ser/grape-path.ser b/imageio/imageio-clippath/src/test/resources/ser/grape-path.ser
new file mode 100644
index 00000000..457d9507
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/ser/grape-path.ser differ
diff --git a/imageio/imageio-clippath/src/test/resources/ser/multiple-clips.ser b/imageio/imageio-clippath/src/test/resources/ser/multiple-clips.ser
new file mode 100644
index 00000000..1315cf9a
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/ser/multiple-clips.ser differ
diff --git a/imageio/imageio-clippath/src/test/resources/tiff/big-endian-bad-clip-everything-clipped.tif b/imageio/imageio-clippath/src/test/resources/tiff/big-endian-bad-clip-everything-clipped.tif
new file mode 100644
index 00000000..b6aa6407
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/tiff/big-endian-bad-clip-everything-clipped.tif differ
diff --git a/imageio/imageio-clippath/src/test/resources/tiff/big-endian-multiple-clips.tif b/imageio/imageio-clippath/src/test/resources/tiff/big-endian-multiple-clips.tif
new file mode 100644
index 00000000..14e2ee79
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/tiff/big-endian-multiple-clips.tif differ
diff --git a/imageio/imageio-clippath/src/test/resources/tiff/little-endian-grape_with_path.tif b/imageio/imageio-clippath/src/test/resources/tiff/little-endian-grape_with_path.tif
new file mode 100644
index 00000000..4e6ed1d4
Binary files /dev/null and b/imageio/imageio-clippath/src/test/resources/tiff/little-endian-grape_with_path.tif differ
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java
index 7dc7f6da..c6904e76 100644
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/TIFF.java
@@ -167,9 +167,28 @@ public interface TIFF {
int TAG_OLD_SUBFILE_TYPE = 255; // Deprecated NO NOT WRITE!
int TAG_SUB_IFD = 330;
+ /**
+ * XMP record.
+ * @see com.twelvemonkeys.imageio.metadata.xmp.XMP
+ */
int TAG_XMP = 700;
+
+ /**
+ * IPTC record.
+ * @see com.twelvemonkeys.imageio.metadata.iptc.IPTC
+ */
int TAG_IPTC = 33723;
+
+ /**
+ * Photoshop image resources.
+ * @see com.twelvemonkeys.imageio.metadata.psd.PSD
+ */
int TAG_PHOTOSHOP = 34377;
+
+ /**
+ * ICC Color Profile.
+ * @see java.awt.color.ICC_Profile
+ */
int TAG_ICC_PROFILE = 34675;
// Microsoft Office Document Imaging (MODI)
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java
index 6e80d69b..ea1b3d0b 100755
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSD.java
@@ -36,6 +36,9 @@ package com.twelvemonkeys.imageio.metadata.psd;
* @version $Id: PSD.java,v 1.0 24.01.12 16:51 haraldk Exp$
*/
public interface PSD {
+ /** PSD 2+ Native format (.PSD) identifier "8BPS" */
+ int SIGNATURE_8BPS = ('8' << 24) + ('B' << 16) + ('P' << 8) + 'S';
+
/** PSD image resource marker "8BIM". */
int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M';
@@ -44,4 +47,7 @@ public interface PSD {
/** ICC profile image resource id. */
int RES_ICC_PROFILE = 0x040f;
+
+ /** PSD Path resource id. */
+ int RES_CLIPPING_PATH = 0x07d0;
}
diff --git a/imageio/pom.xml b/imageio/pom.xml
index 88e541c6..7d9a3561 100644
--- a/imageio/pom.xml
+++ b/imageio/pom.xml
@@ -26,6 +26,7 @@
imageio-core
imageio-metadata
+ imageio-clippath
imageio-bmp