mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-28 00:00:03 -04:00
TMI-60: Support for clip paths in formats containing PSD resources
This commit is contained in:
+242
@@ -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 <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
|
||||
* @author <a href="mailto:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
*/
|
||||
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<List<AdobePathSegment>> subPaths = new ArrayList<List<AdobePathSegment>>();
|
||||
List<AdobePathSegment> 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<AdobePathSegment>(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<List<AdobePathSegment>> paths) {
|
||||
GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD, paths.size());
|
||||
GeneralPath subpath = null;
|
||||
|
||||
for (List<AdobePathSegment> 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);
|
||||
}
|
||||
}
|
||||
+180
@@ -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 <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
|
||||
* @author <a href="mailto:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>Extract a path from an image input stream, {@link #readPath}</li>
|
||||
* <li>Apply a given path to a given {@code BufferedImage} {@link #applyClippingPath}</li>
|
||||
* <li>Read an image with path applied {@link #readClipped}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
|
||||
* @author <a href="mailto:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @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<Integer, java.util.List<String>> segmentIdentifiers = new LinkedHashMap<Integer, java.util.List<String>>();
|
||||
segmentIdentifiers.put(JPEG.APP13, Arrays.asList("Photoshop 3.0"));
|
||||
|
||||
List<JPEGSegment> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user