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