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 old mode 100644 new mode 100755 index a9227bee..8bcaf44a --- 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 @@ -1,244 +1,26 @@ -/* - * 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 of the copyright holder 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 HOLDER 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. + * AdobePathBuilder. * - * @see Adobe Photoshop Path resource format - * @author Jason Palmer, itemMaster LLC - * @author Harald Kuhr + * @deprecated Use {@link AdobePathReader} instead. This class will be removed in a future release. */ public final class AdobePathBuilder { - final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.path.debug")); + private final AdobePathReader delegate; - 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") - )); + this.delegate = new AdobePathReader(data); + } + + public AdobePathBuilder(final DataInput data) { + this.delegate = new AdobePathReader(data); } - /** - * 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); + return delegate.path(); } } diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathReader.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathReader.java new file mode 100755 index 00000000..88a4b3f6 --- /dev/null +++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathReader.java @@ -0,0 +1,248 @@ +/* + * 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 of the copyright holder 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 HOLDER 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 AdobePathReader { + static final 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 AdobePathReader(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 AdobePathReader(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.cpp, P3.ap); + * curveTo(P3.cpl, P4.cpp, P4.ap); curveTo(P4.cpl, P5.cpp, P5.ap); + * curveTo(P5.cpl, 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; + } + + switch (selector) { + case AdobePathSegment.INITIAL_FILL_RULE_RECORD: + case AdobePathSegment.PATH_FILL_RULE_RECORD: + // Spec says Fill rule is ignored by Photoshop + data.skipBytes(24); + return new AdobePathSegment(selector); + case AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD: + case AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD: + int size = data.readUnsignedShort(); + data.skipBytes(22); + return new AdobePathSegment(selector, size); + default: + return new AdobePathSegment( + selector, + toFixedPoint(data.readInt()), + toFixedPoint(data.readInt()), + toFixedPoint(data.readInt()), + toFixedPoint(data.readInt()), + toFixedPoint(data.readInt()), + toFixedPoint(data.readInt()) + ); + } + } + + // TODO: Move to AdobePathSegment + private static double toFixedPoint(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 old mode 100644 new mode 100755 index d5fe8df7..ff3765a3 --- 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 @@ -30,7 +30,7 @@ package com.twelvemonkeys.imageio.path; -import com.twelvemonkeys.lang.Validate; +import static com.twelvemonkeys.lang.Validate.isTrue; /** * Adobe path segment. @@ -40,17 +40,17 @@ import com.twelvemonkeys.lang.Validate; * @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; + static final int CLOSED_SUBPATH_LENGTH_RECORD = 0; + static final int CLOSED_SUBPATH_BEZIER_LINKED = 1; + static final int CLOSED_SUBPATH_BEZIER_UNLINKED = 2; + static final int OPEN_SUBPATH_LENGTH_RECORD = 3; + static final int OPEN_SUBPATH_BEZIER_LINKED = 4; + static final int OPEN_SUBPATH_BEZIER_UNLINKED = 5; + static final int PATH_FILL_RULE_RECORD = 6; + static final int CLIPBOARD_RECORD = 7; + static final int INITIAL_FILL_RULE_RECORD = 8; - public final static String[] SELECTOR_NAMES = { + static final String[] SELECTOR_NAMES = { "Closed subpath length record", "Closed subpath Bezier knot, linked", "Closed subpath Bezier knot, unlinked", @@ -65,10 +65,16 @@ final class AdobePathSegment { final int selector; final int length; + // TODO: Consider keeping these in 8.24FP format + // Control point preceding knot final double cppy; final double cppx; + + // Anchor point final double apy; final double apx; + + // Control point leaving knot final double cply; final double cplx; @@ -79,8 +85,15 @@ final class AdobePathSegment { 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); + AdobePathSegment(int fillRuleSelector) { + this(isTrue(fillRuleSelector == PATH_FILL_RULE_RECORD, fillRuleSelector, "Expected fill rule record (6): %s"), + 0, -1, -1, -1, -1, -1, -1); + } + + AdobePathSegment(final int lengthSelector, final int length) { + this(isTrue(lengthSelector == CLOSED_SUBPATH_LENGTH_RECORD || lengthSelector == OPEN_SUBPATH_LENGTH_RECORD, lengthSelector, "Expected path length record (0 or 3): %s"), + length, + -1, -1, -1, -1, -1, -1); } private AdobePathSegment(final int selector, final int length, @@ -91,15 +104,15 @@ final class AdobePathSegment { switch (selector) { case CLOSED_SUBPATH_LENGTH_RECORD: case OPEN_SUBPATH_LENGTH_RECORD: - Validate.isTrue(length >= 0, length, "Bad size: %d"); + isTrue(length >= 0, length, "Expected positive length: %d"); break; case CLOSED_SUBPATH_BEZIER_LINKED: case CLOSED_SUBPATH_BEZIER_UNLINKED: case OPEN_SUBPATH_BEZIER_LINKED: case OPEN_SUBPATH_BEZIER_UNLINKED: - Validate.isTrue( + isTrue( cppx >= 0 && cppx <= 1 && cppy >= 0 && cppy <= 1, - String.format("Unexpected point: [%f, %f]", cppx ,cppy) + String.format("Expected point in range [0...1]: (%f, %f)", cppx ,cppy) ); break; case PATH_FILL_RULE_RECORD: @@ -107,7 +120,7 @@ final class AdobePathSegment { case INITIAL_FILL_RULE_RECORD: break; default: - throw new IllegalArgumentException("Bad selector: " + selector); + throw new IllegalArgumentException("Unknown selector: " + selector); } this.selector = selector; @@ -173,10 +186,11 @@ final class AdobePathSegment { 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); + return String.format("Len(selector=%s, length=%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]); + + return String.format("Pt(pre=(%.3f, %.3f), knot=(%.3f, %.3f), post=(%.3f, %.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/AdobePathWriter.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java new file mode 100755 index 00000000..0186ab0e --- /dev/null +++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java @@ -0,0 +1,168 @@ +package com.twelvemonkeys.imageio.path; + +import com.twelvemonkeys.imageio.metadata.psd.PSD; + +import java.awt.*; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.twelvemonkeys.imageio.path.AdobePathSegment.*; +import static com.twelvemonkeys.lang.Validate.isTrue; +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * AdobePathWriter + */ +public final class AdobePathWriter { + + private final List segments; + + /** + * Creates an AdobePathWriter for the given path. + *

+ * NOTE: Photoshop paths are stored with the coordinates + * (0,0) representing the top left corner of the image, + * and (1,1) representing the bottom right corner, + * regardless of image dimensions. + *

+ * + * @param path A {@code Path2D} instance that has {@link Path2D#WIND_EVEN_ODD WIND_EVEN_ODD} rule + * and is contained within the rectangle [x=0.0,y=0.0,w=1.0,h=1.0]. + * @throws IllegalArgumentException if {@code path} is {@code null}, + * the paths winding rule is not @link Path2D#WIND_EVEN_ODD} or + * the paths bounding box is outside [x=0.0,y=0.0,w=1.0,h=1.0]. + */ + public AdobePathWriter(final Path2D path) { + notNull(path, "path"); + // TODO: Test if PS really ignores winding rule as documented... Otherwise we could support writing non-zero too. + isTrue(path.getWindingRule() == Path2D.WIND_EVEN_ODD, path.getWindingRule(), "Only even/odd winding rule supported: %d"); + isTrue(new Rectangle(0, 0, 1, 1).contains(path.getBounds2D()), path.getBounds2D(), "Path bounds must be within [x=0,y=0,w=1,h=1]: %s"); + + segments = pathToSegments(path.getPathIterator(null)); + } + + // TODO: Look at the API so that conversion both ways are aligned. The read part builds a path from List... + private static List pathToSegments(final PathIterator pathIterator) { + double[] coords = new double[6]; + AdobePathSegment prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 0,0, 0, 0); + + List subpath = new ArrayList<>(); + List segments = new ArrayList<>(); + segments.add(new AdobePathSegment(PATH_FILL_RULE_RECORD)); + + while (!pathIterator.isDone()) { + int segmentType = pathIterator.currentSegment(coords); + System.out.println("segmentType: " + segmentType); + System.err.println("coords: " + Arrays.toString(coords)); + + switch (segmentType) { + case PathIterator.SEG_MOVETO: + // TODO: What if we didn't close before the moveto? Start new segment here? + + // Dummy starting point, will be updated later + prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, coords[1], coords[0], 0, 0); + break; + + case PathIterator.SEG_LINETO: + subpath.add(new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, prev.apy, prev.apx, coords[1], coords[0])); + prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, coords[1], coords[0], coords[1], coords[0], 0, 0); + break; + + case PathIterator.SEG_QUADTO: + subpath.add(new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, prev.apy, prev.apx, coords[1], coords[0])); + prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, coords[3], coords[2], coords[3], coords[2], 0, 0); + break; + + case PathIterator.SEG_CUBICTO: + subpath.add(new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, prev.apy, prev.apx, coords[1], coords[0])); + prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, coords[3], coords[2], coords[5], coords[4], 0, 0); + break; + + case PathIterator.SEG_CLOSE: + // Replace initial point. + AdobePathSegment initial = subpath.get(0); + if (initial.apx != prev.apx || initial.apy != prev.apy) { + // TODO: Line back to initial if last anchor point does not equal initial anchor? +// subpath.add(new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, prev.apy, prev.apx, 0, 0)); + System.err.println("FOO!"); + } + subpath.set(0, new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, initial.apy, initial.apx, initial.cply, initial.cplx)); + + // Add to full path + segments.add(new AdobePathSegment(CLOSED_SUBPATH_LENGTH_RECORD, subpath.size())); + segments.addAll(subpath); + + subpath.clear(); + + break; + } + + pathIterator.next(); + } + + return segments; + } + + public void writePath(final DataOutput output) throws IOException { + System.err.println("segments: " + segments.size()); + + output.writeInt(PSD.RESOURCE_TYPE); + output.writeShort(PSD.RES_CLIPPING_PATH); + output.writeShort(0); // Path name (Pascal string) empty + pad + output.writeInt(segments.size() * 26); // Resource size + + + for (AdobePathSegment segment : segments) { + System.err.println(segment); + switch (segment.selector) { + case PATH_FILL_RULE_RECORD: + case INITIAL_FILL_RULE_RECORD: + // The first 26-byte path record contains a selector value of 6, path fill rule record. + // The remaining 24 bytes of the first record are zeroes. Paths use even/odd ruling. + output.writeShort(segment.selector); + output.write(new byte[24]); + break; + case OPEN_SUBPATH_LENGTH_RECORD: + case CLOSED_SUBPATH_LENGTH_RECORD: + output.writeShort(segment.selector); + output.writeShort(segment.length); // Subpath length + output.write(new byte[22]); + break; + default: + output.writeShort(segment.selector); + output.writeInt(toFixedPoint(segment.cppy)); + output.writeInt(toFixedPoint(segment.cppx)); + output.writeInt(toFixedPoint(segment.apy)); + output.writeInt(toFixedPoint(segment.apx)); + output.writeInt(toFixedPoint(segment.cply)); + output.writeInt(toFixedPoint(segment.cplx)); + break; + } + } + } + + public byte[] createPath() { + // TODO: Do we need to care about endianness for TIFF files? + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try (DataOutputStream stream = new DataOutputStream(bytes)) { + writePath(stream); + } catch (IOException e) { + throw new AssertionError("Should never.. uh.. Oh well. It happened.", e); + } + + return bytes.toByteArray(); + } + + // TODO: Move to AdobePathSegment + private static int toFixedPoint(final double value) { + return (int) Math.round(value * 0x1000000); + } +} 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 old mode 100644 new mode 100755 index 8d30112a..e4d62c62 --- 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 @@ -69,7 +69,7 @@ import static java.util.Collections.singletonList; * * * @see Adobe Photoshop Path resource format - * @see com.twelvemonkeys.imageio.path.AdobePathBuilder + * @see AdobePathReader * @author Jason Palmer, itemMaster LLC * @author Harald Kuhr * @author last modified by $Author: harald.kuhr$ @@ -90,7 +90,7 @@ public final class Paths { * @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 + * @see AdobePathReader */ public static Path2D readPath(final ImageInputStream stream) throws IOException { notNull(stream, "stream"); @@ -99,7 +99,7 @@ public final class Paths { if (magic == PSD.RESOURCE_TYPE) { // This is a PSD Image Resource Block, we can parse directly - return buildPathFromPhotoshopResources(stream); + return readPathFromPhotoshopResources(stream); } else if (magic == PSD.SIGNATURE_8BPS) { // PSD version @@ -115,7 +115,7 @@ public final class Paths { long imageResourcesLen = stream.readUnsignedInt(); // Image resources - return buildPathFromPhotoshopResources(new SubImageInputStream(stream, imageResourcesLen)); + return readPathFromPhotoshopResources(new SubImageInputStream(stream, imageResourcesLen)); } else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) { // JPEG version @@ -125,7 +125,7 @@ public final class Paths { List photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers); if (!photoshop.isEmpty()) { - return buildPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data())); + return readPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data())); } } else if (magic >>> 16 == TIFF.BYTE_ORDER_MARK_BIG_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC @@ -137,7 +137,7 @@ public final class Paths { Entry photoshop = directory.getEntryById(TIFF.TAG_PHOTOSHOP); if (photoshop != null) { - return buildPathFromPhotoshopResources(new ByteArrayImageInputStream((byte[]) photoshop.getValue())); + return readPathFromPhotoshopResources(new ByteArrayImageInputStream((byte[]) photoshop.getValue())); } } @@ -156,17 +156,17 @@ public final class Paths { } } - private static Path2D buildPathFromPhotoshopResources(final ImageInputStream stream) throws IOException { + private static Path2D readPathFromPhotoshopResources(final ImageInputStream stream) throws IOException { Directory resourceBlocks = new PSDReader().read(stream); - if (AdobePathBuilder.DEBUG) { + if (AdobePathReader.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 new AdobePathReader((byte[]) resourceBlock.getValue()).path(); } return null; @@ -256,7 +256,19 @@ public final class Paths { // Test code public static void main(final String[] args) throws IOException, InterruptedException { - BufferedImage destination = readClipped(ImageIO.createImageInputStream(new File(args[0]))); + BufferedImage destination; + if (args.length == 1) { + // Embedded path + try (ImageInputStream input = ImageIO.createImageInputStream(new File(args[0]))) { + destination = readClipped(input); + } + } + else { + // Separate path and image + try (ImageInputStream input = ImageIO.createImageInputStream(new File(args[1]))) { + destination = applyClippingPath(readPath(input), ImageIO.read(new File(args[0]))); + } + } File tempFile = File.createTempFile("clipped-", ".png"); tempFile.deleteOnExit();