From 859b232f6427edeaf8c584ed99a2ea91ec8657d2 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 2 Jan 2020 15:24:22 +0100 Subject: [PATCH 01/17] #490: Initial commit Adobe Path write. --- .../imageio/path/AdobePathBuilder.java | 236 +---------------- .../imageio/path/AdobePathReader.java | 248 ++++++++++++++++++ .../imageio/path/AdobePathSegment.java | 52 ++-- .../imageio/path/AdobePathWriter.java | 168 ++++++++++++ .../com/twelvemonkeys/imageio/path/Paths.java | 32 ++- 5 files changed, 480 insertions(+), 256 deletions(-) mode change 100644 => 100755 imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathBuilder.java create mode 100755 imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathReader.java mode change 100644 => 100755 imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathSegment.java create mode 100755 imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java mode change 100644 => 100755 imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java 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(); From eec8268eb94af8197a1ad628fda0ddfe9524d757 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 2 Jan 2020 15:26:48 +0100 Subject: [PATCH 02/17] #490: Adobe Path PoC --- .../imageio/path/AdobePathBuilder.java | 18 ++++++++++++++---- .../imageio/path/AdobePathReader.java | 16 +++++++++------- .../imageio/path/AdobePathSegment.java | 3 +-- .../imageio/path/AdobePathWriter.java | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) 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 index 8bcaf44a..44213335 100755 --- 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 @@ -10,16 +10,26 @@ import java.io.IOException; * @deprecated Use {@link AdobePathReader} instead. This class will be removed in a future release. */ public final class AdobePathBuilder { + private final AdobePathReader delegate; - public AdobePathBuilder(final byte[] data) { - this.delegate = new AdobePathReader(data); - } - + /** + * @see AdobePathReader#AdobePathReader(DataInput) + */ public AdobePathBuilder(final DataInput data) { this.delegate = new AdobePathReader(data); } + /** + * @see AdobePathReader#AdobePathReader(byte[]) + */ + public AdobePathBuilder(final byte[] data) { + this.delegate = new AdobePathReader(data); + } + + /** + * @see AdobePathReader#path() + */ public Path2D path() throws IOException { 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 index 88a4b3f6..93472acd 100755 --- 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 @@ -57,8 +57,8 @@ public final class AdobePathReader { private final DataInput data; /** - * Creates a path builder that will read its data from a {@code DataInput}, such as an - * {@code ImageInputStream}. + * Creates a path reader 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. @@ -70,7 +70,7 @@ public final class AdobePathReader { } /** - * Creates a path builder that will read its data from a {@code byte} array. + * Creates a path reader 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. @@ -84,14 +84,14 @@ public final class AdobePathReader { } /** - * Builds the path. + * Builds the path by reading from the supplied input. * * @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> subPaths = new ArrayList<>(); List currentPath = null; int currentPathLength = 0; @@ -110,7 +110,7 @@ public final class AdobePathReader { subPaths.add(currentPath); } - currentPath = new ArrayList(segment.length); + currentPath = new ArrayList<>(segment.length); currentPathLength = segment.length; } else if (segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED @@ -137,7 +137,7 @@ public final class AdobePathReader { subPaths.add(currentPath); } - // now we have collected the PathPoints now create a Shape. + // We have collected the Path points, now create a Shape return pathToShape(subPaths); } @@ -199,6 +199,8 @@ public final class AdobePathReader { break; } + default: + throw new AssertionError(); } } } 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 index ff3765a3..dae391a9 100755 --- 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 @@ -86,7 +86,7 @@ final class AdobePathSegment { } AdobePathSegment(int fillRuleSelector) { - this(isTrue(fillRuleSelector == PATH_FILL_RULE_RECORD, fillRuleSelector, "Expected fill rule record (6): %s"), + this(isTrue(fillRuleSelector == PATH_FILL_RULE_RECORD || fillRuleSelector == INITIAL_FILL_RULE_RECORD, fillRuleSelector, "Expected fill rule record (6 or 8): %s"), 0, -1, -1, -1, -1, -1, -1); } @@ -190,7 +190,6 @@ final class AdobePathSegment { default: // fall-through } - 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 index 0186ab0e..7ab95a95 100755 --- 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 @@ -91,7 +91,7 @@ public final class AdobePathWriter { 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!"); + throw new AssertionError("Not a closed path"); } subpath.set(0, new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, initial.apy, initial.apx, initial.cply, initial.cplx)); From 6be86affd6c3f95bbc0e9dcfd65e35a52724021b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 2 Jan 2020 15:32:06 +0100 Subject: [PATCH 03/17] #490: Fixed tests --- .../twelvemonkeys/imageio/path/AdobePathSegmentTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index c42aee6a..df7d98c1 100644 --- 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 @@ -195,11 +195,11 @@ public class AdobePathSegmentTest { @Test public void testToStringRule() { - String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD, 2).toString(); + String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD).toString(); assertTrue(string, string.startsWith("Rule")); assertTrue(string, string.contains("Initial")); assertTrue(string, string.contains("fill")); - assertTrue(string, string.contains("rule=2")); + assertTrue(string, string.contains("rule=0")); } @Test @@ -208,13 +208,13 @@ public class AdobePathSegmentTest { assertTrue(string, string.startsWith("Len")); assertTrue(string, string.contains("Closed")); assertTrue(string, string.contains("subpath")); - assertTrue(string, string.contains("totalPoints=2")); + assertTrue(string, string.contains("length=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")); + assertTrue(string, string.contains("length=42")); } @Test From f15bcc7df91b324e1cc721c09c6e6291a58f144b Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Tue, 7 Jan 2020 13:44:40 +0100 Subject: [PATCH 04/17] log4j removal --- sandbox/sandbox-servlet/pom.xml | 6 - .../servlet/log4j/Log4JContextWrapper.java | 183 ------------------ servlet/pom.xml | 7 - 3 files changed, 196 deletions(-) delete mode 100755 sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java diff --git a/sandbox/sandbox-servlet/pom.xml b/sandbox/sandbox-servlet/pom.xml index 9d05293f..d2bb73de 100644 --- a/sandbox/sandbox-servlet/pom.xml +++ b/sandbox/sandbox-servlet/pom.xml @@ -80,11 +80,5 @@ provided - - log4j - log4j - 1.2.14 - provided - diff --git a/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java b/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java deleted file mode 100755 index 0599a468..00000000 --- a/sandbox/sandbox-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.twelvemonkeys.servlet.log4j; - -import org.apache.log4j.Logger; - -import javax.servlet.RequestDispatcher; -import javax.servlet.Servlet; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import java.io.InputStream; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Enumeration; -import java.util.Set; - -/** - * Log4JContextWrapper - *

- * - * @author Harald Kuhr - * @version $Id: log4j/Log4JContextWrapper.java#1 $ - */ -final class Log4JContextWrapper implements ServletContext { - // TODO: Move to sandbox - - // TODO: This solution sucks... - // How about starting to create some kind of pluggable decorator system, - // something along the lines of AOP mixins/interceptor pattern.. - // Probably using a dynamic Proxy, delegating to the mixins and or the - // wrapped object based on configuration. - // This way we could simply call ServletUtil.decorate(ServletContext):ServletContext - // And the context would be decorated with all configured mixins at once, - // requiring less boilerplate delegation code, and less layers of wrapping - // (alternatively we could decorate the Servlet/FilterConfig objects). - // See the ServletUtil.createWrapper methods for some hints.. - - - // Something like this: - public static ServletContext wrap(final ServletContext pContext, final Object[] pDelegates, final ClassLoader pLoader) { - ClassLoader cl = pLoader != null ? pLoader : Thread.currentThread().getContextClassLoader(); - - // TODO: Create a "static" mapping between methods in the ServletContext - // and the corresponding delegate - - // TODO: Resolve super-invokations, to delegate to next delegate in - // chain, and finally invoke pContext - - return (ServletContext) Proxy.newProxyInstance(cl, new Class[] {ServletContext.class}, new InvocationHandler() { - public Object invoke(Object pProxy, Method pMethod, Object[] pArgs) throws Throwable { - // TODO: Test if any of the delegates should receive, if so invoke - - // Else, invoke on original object - return pMethod.invoke(pContext, pArgs); - } - }); - } - - private final ServletContext context; - - private final Logger logger; - - Log4JContextWrapper(ServletContext pContext) { - context = pContext; - - // TODO: We want a logger per servlet, not per servlet context, right? - logger = Logger.getLogger(pContext.getServletContextName()); - - // TODO: Automatic init/config of Log4J using context parameter for log4j.xml? - // See Log4JInit.java - - // TODO: Automatic config of properties in the context wrapper? - } - - public final void log(final Exception pException, final String pMessage) { - log(pMessage, pException); - } - - // TODO: Add more logging methods to interface info/warn/error? - // TODO: Implement these mehtods in GenericFilter/GenericServlet? - - public void log(String pMessage) { - // TODO: Get logger for caller.. - // Should be possible using some stack peek hack, but that's slow... - // Find a good way... - // Maybe just pass it into the constuctor, and have one wrapper per servlet - logger.info(pMessage); - } - - public void log(String pMessage, Throwable pCause) { - // TODO: Get logger for caller.. - - logger.error(pMessage, pCause); - } - - public Object getAttribute(String pMessage) { - return context.getAttribute(pMessage); - } - - public Enumeration getAttributeNames() { - return context.getAttributeNames(); - } - - public ServletContext getContext(String pMessage) { - return context.getContext(pMessage); - } - - public String getInitParameter(String pMessage) { - return context.getInitParameter(pMessage); - } - - public Enumeration getInitParameterNames() { - return context.getInitParameterNames(); - } - - public int getMajorVersion() { - return context.getMajorVersion(); - } - - public String getMimeType(String pMessage) { - return context.getMimeType(pMessage); - } - - public int getMinorVersion() { - return context.getMinorVersion(); - } - - public RequestDispatcher getNamedDispatcher(String pMessage) { - return context.getNamedDispatcher(pMessage); - } - - public String getRealPath(String pMessage) { - return context.getRealPath(pMessage); - } - - public RequestDispatcher getRequestDispatcher(String pMessage) { - return context.getRequestDispatcher(pMessage); - } - - public URL getResource(String pMessage) throws MalformedURLException { - return context.getResource(pMessage); - } - - public InputStream getResourceAsStream(String pMessage) { - return context.getResourceAsStream(pMessage); - } - - public Set getResourcePaths(String pMessage) { - return context.getResourcePaths(pMessage); - } - - public String getServerInfo() { - return context.getServerInfo(); - } - - public Servlet getServlet(String pMessage) throws ServletException { - //noinspection deprecation - return context.getServlet(pMessage); - } - - public String getServletContextName() { - return context.getServletContextName(); - } - - public Enumeration getServletNames() { - //noinspection deprecation - return context.getServletNames(); - } - - public Enumeration getServlets() { - //noinspection deprecation - return context.getServlets(); - } - - public void removeAttribute(String pMessage) { - context.removeAttribute(pMessage); - } - - public void setAttribute(String pMessage, Object pExtension) { - context.setAttribute(pMessage, pExtension); - } -} diff --git a/servlet/pom.xml b/servlet/pom.xml index 61ff3353..9aec6f96 100644 --- a/servlet/pom.xml +++ b/servlet/pom.xml @@ -57,13 +57,6 @@ provided - - log4j - log4j - 1.2.14 - provided - - commons-fileupload commons-fileupload From 51ace4ca7fea7537be5cd7d3a6b617e225eefbd7 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 9 Jan 2020 19:17:35 +0100 Subject: [PATCH 05/17] #490: Refactorings, added initial detection of linked/unlinked segments, more tests. --- .../imageio/path/AdobePathBuilder.java | 4 +- .../imageio/path/AdobePathReader.java | 23 +- .../imageio/path/AdobePathSegment.java | 34 +- .../imageio/path/AdobePathWriter.java | 107 +++++-- .../com/twelvemonkeys/imageio/path/Paths.java | 11 +- .../imageio/path/AdobePathBuilderTest.java | 1 + .../imageio/path/AdobePathReaderTest.java | 165 ++++++++++ .../imageio/path/AdobePathSegmentTest.java | 14 +- .../imageio/path/AdobePathWriterTest.java | 290 ++++++++++++++++++ .../twelvemonkeys/imageio/path/PathsTest.java | 29 +- .../imageio/metadata/psd/PSDReader.java | 1 - 11 files changed, 599 insertions(+), 80 deletions(-) create mode 100644 imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java create mode 100644 imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java 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 index 44213335..0a43af92 100755 --- 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 @@ -28,9 +28,9 @@ public final class AdobePathBuilder { } /** - * @see AdobePathReader#path() + * @see AdobePathReader#readPath() */ public Path2D path() throws IOException { - return delegate.path(); + return delegate.readPath(); } } 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 index 93472acd..cae9d8dd 100755 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Harald Kuhr + * Copyright (c) 2014-2020, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,7 +33,6 @@ 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; @@ -90,7 +89,7 @@ public final class AdobePathReader { * @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 { + public Path2D readPath() throws IOException { List> subPaths = new ArrayList<>(); List currentPath = null; int currentPathLength = 0; @@ -110,8 +109,8 @@ public final class AdobePathReader { subPaths.add(currentPath); } - currentPath = new ArrayList<>(segment.length); - currentPathLength = segment.length; + currentPath = new ArrayList<>(segment.lengthOrRule); + currentPathLength = segment.lengthOrRule; } else if (segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED || segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED @@ -149,8 +148,8 @@ public final class AdobePathReader { * closePath() */ private Path2D pathToShape(final List> paths) { - GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD, paths.size()); - GeneralPath subpath = null; + Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD, paths.size()); + Path2D subpath = null; for (List points : paths) { int length = points.size(); @@ -163,7 +162,7 @@ public final class AdobePathReader { switch (step) { // Begin case 0: { - subpath = new GeneralPath(Path2D.WIND_EVEN_ODD, length); + subpath = new Path2D.Float(Path2D.WIND_EVEN_ODD, length); subpath.moveTo(current.apx, current.apy); if (length > 1) { @@ -222,14 +221,12 @@ public final class AdobePathReader { 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); + // Spec says Fill rule is ignored by Photoshop, we'll read it anyway case AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD: case AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD: - int size = data.readUnsignedShort(); + int lengthOrRule = data.readUnsignedShort(); data.skipBytes(22); - return new AdobePathSegment(selector, size); + return new AdobePathSegment(selector, lengthOrRule); default: return new AdobePathSegment( selector, 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 index dae391a9..d5cc82cf 100755 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Harald Kuhr + * Copyright (c) 2014-2020, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -63,7 +63,7 @@ final class AdobePathSegment { }; final int selector; - final int length; + final int lengthOrRule; // TODO: Consider keeping these in 8.24FP format // Control point preceding knot @@ -85,18 +85,14 @@ final class AdobePathSegment { this(selector, -1, cppy, cppx, apy, apx, cply, cplx); } - AdobePathSegment(int fillRuleSelector) { - this(isTrue(fillRuleSelector == PATH_FILL_RULE_RECORD || fillRuleSelector == INITIAL_FILL_RULE_RECORD, fillRuleSelector, "Expected fill rule record (6 or 8): %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, + AdobePathSegment(final int selector, final int lengthOrRule) { + this(isTrue(selector == CLOSED_SUBPATH_LENGTH_RECORD || selector == OPEN_SUBPATH_LENGTH_RECORD + || selector == PATH_FILL_RULE_RECORD || selector == INITIAL_FILL_RULE_RECORD, selector, "Expected path length or fill rule record (0/3 or 6/8): %s"), + lengthOrRule, -1, -1, -1, -1, -1, -1); } - private AdobePathSegment(final int selector, final int length, + private AdobePathSegment(final int selector, final int lengthOrRule, final double cppy, final double cppx, final double apy, final double apx, final double cply, final double cplx) { @@ -104,7 +100,7 @@ final class AdobePathSegment { switch (selector) { case CLOSED_SUBPATH_LENGTH_RECORD: case OPEN_SUBPATH_LENGTH_RECORD: - isTrue(length >= 0, length, "Expected positive length: %d"); + isTrue(lengthOrRule >= 0, lengthOrRule, "Expected positive length: %d"); break; case CLOSED_SUBPATH_BEZIER_LINKED: case CLOSED_SUBPATH_BEZIER_UNLINKED: @@ -116,15 +112,17 @@ final class AdobePathSegment { ); break; case PATH_FILL_RULE_RECORD: - case CLIPBOARD_RECORD: case INITIAL_FILL_RULE_RECORD: + isTrue(lengthOrRule == 0 || lengthOrRule == 1, lengthOrRule, "Expected rule (1 or 0): %d"); + break; + case CLIPBOARD_RECORD: break; default: throw new IllegalArgumentException("Unknown selector: " + selector); } this.selector = selector; - this.length = length; + this.lengthOrRule = lengthOrRule; this.cppy = cppy; this.cppx = cppx; this.apy = apy; @@ -152,7 +150,7 @@ final class AdobePathSegment { && Double.compare(that.cppx, cppx) == 0 && Double.compare(that.cppy, cppy) == 0 && selector == that.selector - && length == that.length; + && lengthOrRule == that.lengthOrRule; } @@ -161,7 +159,7 @@ final class AdobePathSegment { long tempBits; int result = selector; - result = 31 * result + length; + result = 31 * result + lengthOrRule; tempBits = Double.doubleToLongBits(cppy); result = 31 * result + (int) (tempBits ^ (tempBits >>> 32)); tempBits = Double.doubleToLongBits(cppx); @@ -183,10 +181,10 @@ final class AdobePathSegment { switch (selector) { case INITIAL_FILL_RULE_RECORD: case PATH_FILL_RULE_RECORD: - return String.format("Rule(selector=%s, rule=%d)", SELECTOR_NAMES[selector], length); + return String.format("Rule(selector=%s, rule=%d)", SELECTOR_NAMES[selector], lengthOrRule); case CLOSED_SUBPATH_LENGTH_RECORD: case OPEN_SUBPATH_LENGTH_RECORD: - return String.format("Len(selector=%s, length=%d)", SELECTOR_NAMES[selector], length); + return String.format("Len(selector=%s, length=%d)", SELECTOR_NAMES[selector], lengthOrRule); default: // fall-through } 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 index 7ab95a95..735ffee7 100755 --- 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 @@ -1,3 +1,33 @@ +/* + * Copyright (c) 2020 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.metadata.psd.PSD; @@ -13,6 +43,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import static com.twelvemonkeys.imageio.path.AdobePathReader.DEBUG; import static com.twelvemonkeys.imageio.path.AdobePathSegment.*; import static com.twelvemonkeys.lang.Validate.isTrue; import static com.twelvemonkeys.lang.Validate.notNull; @@ -27,10 +58,10 @@ public final class AdobePathWriter { /** * 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. + * 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 @@ -51,16 +82,24 @@ public final class AdobePathWriter { // 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); + 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)); + segments.add(new AdobePathSegment(PATH_FILL_RULE_RECORD, 0)); + segments.add(new AdobePathSegment(INITIAL_FILL_RULE_RECORD, 0)); while (!pathIterator.isDone()) { int segmentType = pathIterator.currentSegment(coords); - System.out.println("segmentType: " + segmentType); - System.err.println("coords: " + Arrays.toString(coords)); + + if (DEBUG) { + System.out.println("segmentType: " + segmentType); + System.err.println("coords: " + Arrays.toString(coords)); + } + + // TODO: We need to support unlinked segments! + + boolean collinear; switch (segmentType) { case PathIterator.SEG_MOVETO: @@ -71,17 +110,23 @@ public final class AdobePathWriter { 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])); + collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); + System.out.println("isCollinear? " + collinear); + subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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])); + collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); + System.out.println("isCollinear? " + collinear); + subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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])); + collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); + System.out.println("isCollinear? " + collinear); + subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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; @@ -93,7 +138,10 @@ public final class AdobePathWriter { // subpath.add(new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, prev.apy, prev.apx, 0, 0)); throw new AssertionError("Not a closed path"); } - subpath.set(0, new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, prev.cppy, prev.cppx, initial.apy, initial.apx, initial.cply, initial.cplx)); + + collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.cplx, initial.cply); + System.out.println("isCollinear? " + collinear); + subpath.set(0, new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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())); @@ -110,17 +158,38 @@ public final class AdobePathWriter { return segments; } - public void writePath(final DataOutput output) throws IOException { - System.err.println("segments: " + segments.size()); + private static final double COLLINEARITY_THRESHOLD = 0.035; + private static boolean isCollinearAndSameDistance(double x1, double y1, double x2, double y2, double x3, double y3) { +// return (y3 - y2) * (x2 - x1) == (y2 - y1) * (x3 - x2); // Collinear Slope +// return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) == 0; // Collinear (Double) Area + +// return Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) <= 0.0005; // With some slack... + +// return Math.abs(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) - Math.sqrt(Math.pow(x3 - x2, 2) + Math.pow(y3 - y2, 2))) <= 0.01; + + // TODO: Get hold of a real Photoshop sample... The current data may be wrong. + // TODO: If correct, PS writes linked if all points are the same... + return (x1 == x2 && x2 == x3 && y1 == y2 && y2 == y3) || + (x1 != x2 || y1 != y2) && (x2 != x3 || y2 != y3) && Math.abs((x2 - x1) - (x3 - x2)) <= COLLINEARITY_THRESHOLD && Math.abs((y2 - y1) - (y3 - y2)) <= COLLINEARITY_THRESHOLD; + } + + void writePathResource(final DataOutput output) throws IOException { 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 + writePath(output); + } + + public void writePath(final DataOutput output) throws IOException { + if (DEBUG) { + System.err.println("segments: " + segments.size()); + System.err.println(segments); + } for (AdobePathSegment segment : segments) { - System.err.println(segment); switch (segment.selector) { case PATH_FILL_RULE_RECORD: case INITIAL_FILL_RULE_RECORD: @@ -132,7 +201,7 @@ public final class AdobePathWriter { case OPEN_SUBPATH_LENGTH_RECORD: case CLOSED_SUBPATH_LENGTH_RECORD: output.writeShort(segment.selector); - output.writeShort(segment.length); // Subpath length + output.writeShort(segment.lengthOrRule); // Subpath length output.write(new byte[22]); break; default: @@ -148,13 +217,15 @@ public final class AdobePathWriter { } } - public byte[] createPath() { + // TODO: Better name? + public byte[] writePath() { // 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) { + } + catch (IOException e) { throw new AssertionError("Should never.. uh.. Oh well. It happened.", e); } 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 index e4d62c62..7cf3e33f 100755 --- 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 @@ -163,10 +163,10 @@ public final class Paths { System.out.println("resourceBlocks: " + resourceBlocks); } - Entry resourceBlock = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH); + Entry pathResource = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH); - if (resourceBlock != null) { - return new AdobePathReader((byte[]) resourceBlock.getValue()).path(); + if (pathResource != null) { + return new AdobePathReader((byte[]) pathResource.getValue()).readPath(); } return null; @@ -259,9 +259,7 @@ public final class Paths { BufferedImage destination; if (args.length == 1) { // Embedded path - try (ImageInputStream input = ImageIO.createImageInputStream(new File(args[0]))) { - destination = readClipped(input); - } + destination = readClipped(ImageIO.createImageInputStream(new File(args[0]))); } else { // Separate path and image @@ -282,5 +280,4 @@ public final class Paths { 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 index 1e917a86..ddd05dc5 100644 --- 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 @@ -43,6 +43,7 @@ import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals; import static com.twelvemonkeys.imageio.path.PathsTest.readExpectedPath; import static org.junit.Assert.assertNotNull; +@SuppressWarnings("deprecation") public class AdobePathBuilderTest { @Test(expected = IllegalArgumentException.class) diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java new file mode 100644 index 00000000..2837bbe9 --- /dev/null +++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2020 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 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 AdobePathReaderTest { + + @Test(expected = IllegalArgumentException.class) + public void testCreateNullBytes() { + new AdobePathReader((byte[]) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNull() { + new AdobePathReader((DataInput) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateEmpty() { + new AdobePathReader(new byte[0]); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateShortPath() { + new AdobePathReader(new byte[3]); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateImpossiblePath() { + new AdobePathReader(new byte[7]); + } + + @Test + public void testCreate() { + new AdobePathReader(new byte[52]); + } + + @Test + public void testNoPath() throws IOException { + Path2D path = new AdobePathReader(new byte[26]).readPath(); + 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 AdobePathReader(data).readPath(); + 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 AdobePathReader(data).readPath(); + 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 AdobePathReader(data).readPath(); + 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 AdobePathReader(data).readPath(); + 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 AdobePathReader(data).readPath(); + + 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 AdobePathReader(data).readPath(); + + 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 index df7d98c1..7b4827e4 100644 --- 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 @@ -63,7 +63,7 @@ public class AdobePathSegmentTest { AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42); assertEquals(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, segment.selector); - assertEquals(42, segment.length); + assertEquals(42, segment.lengthOrRule); assertEquals(-1, segment.cppx, 0); assertEquals(-1, segment.cppy, 0); assertEquals(-1, segment.apx, 0); @@ -82,7 +82,7 @@ public class AdobePathSegmentTest { AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 27); assertEquals(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, segment.selector); - assertEquals(27, segment.length); + assertEquals(27, segment.lengthOrRule); assertEquals(-1, segment.cppx, 0); assertEquals(-1, segment.cppy, 0); assertEquals(-1, segment.apx, 0); @@ -98,7 +98,7 @@ public class AdobePathSegmentTest { 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(-1, segment.lengthOrRule); assertEquals(.5, segment.cppx, 0); assertEquals(.5, segment.cppy, 0); assertEquals(0, segment.apx, 0); @@ -122,7 +122,7 @@ public class AdobePathSegmentTest { 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(-1, segment.lengthOrRule); assertEquals(.5, segment.cppx, 0); assertEquals(.5, segment.cppy, 0); assertEquals(0, segment.apx, 0); @@ -149,7 +149,7 @@ public class AdobePathSegmentTest { 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(-1, segment.lengthOrRule); assertEquals(.5, segment.cppx, 0); assertEquals(.5, segment.cppy, 0); assertEquals(0, segment.apx, 0); @@ -173,7 +173,7 @@ public class AdobePathSegmentTest { 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(-1, segment.lengthOrRule); assertEquals(.5, segment.cppx, 0); assertEquals(.5, segment.cppy, 0); assertEquals(0, segment.apx, 0); @@ -195,7 +195,7 @@ public class AdobePathSegmentTest { @Test public void testToStringRule() { - String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD).toString(); + String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD, 0).toString(); assertTrue(string, string.startsWith("Rule")); assertTrue(string, string.contains("Initial")); assertTrue(string, string.contains("fill")); diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java new file mode 100644 index 00000000..f46d02c2 --- /dev/null +++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2020 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 org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.geom.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import static com.twelvemonkeys.imageio.path.AdobePathSegment.*; +import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * AdobePathWriterTest. + * + * @author Harald Kuhr + * @author last modified by haraldk: harald.kuhr$ + * @version : AdobePathWriterTest.java,v 1.0 2020-01-02 harald.kuhr Exp$ + */ +public class AdobePathWriterTest { + + @Test(expected = IllegalArgumentException.class) + public void testCreateWriterNull() { + new AdobePathWriter(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateWriterInvalid() { + new AdobePathWriter(new Path2D.Double(Path2D.WIND_NON_ZERO)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateWriterOutOfBounds() { + Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD); + path.append(new Ellipse2D.Double(.5, 0.5, 2, 2), false); + + new AdobePathWriter(path); + } + + @Test + public void testCreateWriterValid() { + Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD); + path.append(new Ellipse2D.Double(.25, .25, .5, .5), false); + + new AdobePathWriter(path); + } + + @Test + public void testCreateWriterMulti() { + Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.append(new Ellipse2D.Float(.25f, .25f, .5f, .5f), false); + path.append(new Rectangle2D.Double(0, 0, 1, .5), false); + path.append(new Polygon(new int[] {1, 2, 0, 1}, new int[] {0, 2, 2, 0}, 4) + .getPathIterator(AffineTransform.getScaleInstance(1 / 2.0, 1 / 2.0)), false); + + new AdobePathWriter(path); + } + + @Test + public void testWriteToStream() throws IOException { + Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.append(new Ellipse2D.Double(0, 0, 1, 1), false); + path.append(new Ellipse2D.Double(.5, .5, .5, .5), false); + path.append(new Ellipse2D.Double(.25, .25, .5, .5), false); + + AdobePathWriter pathCreator = new AdobePathWriter(path); + + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + try (ImageOutputStream output = ImageIO.createImageOutputStream(byteStream)) { + pathCreator.writePath(output); + } + + assertEquals(17 * 26, byteStream.size()); + + byte[] bytes = byteStream.toByteArray(); + + int off = 0; + + // Path/initial fill rule: Even-Odd (0) + assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Elipse 1: 0, 0, 1, 1 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 57, 78, -68, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 0, -58, -79, 68, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 1, 0, 0, 0, 0, -58, -79, 68, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 0, 57, 78, -68}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -58, -79, 68, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 57, 78, -68, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 0, 0, 0, 57, 78, -68, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, -58, -79, 68}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Elipse 2: .5, .5, .5, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -100, -89, 94, 1, 0, 0, 0, 0, -64, 0, 0, 1, 0, 0, 0, 0, -29, 88, -94, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 1, 0, 0, 0, 0, -29, 88, -94, 1, 0, 0, 0, 0, -64, 0, 0, 1, 0, 0, 0, 0, -100, -89, 94}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -29, 88, -94, 0, -128, 0, 0, 0, -64, 0, 0, 0, -128, 0, 0, 0, -100, -89, 94, 0, -128, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -128, 0, 0, 0, -100, -89, 94, 0, -128, 0, 0, 0, -64, 0, 0, 0, -128, 0, 0, 0, -29, 88, -94}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Elipse32: .25, .25, .5, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 92, -89, 94, 0, -64, 0, 0, 0, -128, 0, 0, 0, -64, 0, 0, 0, -93, 88, -94, 0, -64, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -64, 0, 0, 0, -93, 88, -94, 0, -64, 0, 0, 0, -128, 0, 0, 0, -64, 0, 0, 0, 92, -89, 94}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -93, 88, -94, 0, 64, 0, 0, 0, -128, 0, 0, 0, 64, 0, 0, 0, 92, -89, 94, 0, 64, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 64, 0, 0, 0, 92, -89, 94, 0, 64, 0, 0, 0, -128, 0, 0, 0, 64, 0, 0, 0, -93, 88, -94}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Sanity + assertEquals(bytes.length, off); + } + + @Test + public void testCreateArray() { + Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.append(new Rectangle2D.Double(0, 0, 1, .5), false); + path.append(new Rectangle2D.Double(.25, .25, .5, .5), false); + + AdobePathWriter pathCreator = new AdobePathWriter(path); + + byte[] bytes = pathCreator.writePath(); + System.err.println(Arrays.toString(bytes)); + + assertEquals(12 * 26, bytes.length); + + int off = 0; + + // Path/initial fill rule: Even-Odd (0) + assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Rectangle 1: 0, 0, 1, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Rectangle 2: .25, .25, .5, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, -64, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 64, 0, 0, 0, -64, 0, 0, 0, 64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, 64, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -64, 0, 0, 0, 64, 0, 0, 0, -64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Sanity + assertEquals(bytes.length, off); + } + + @Test + public void testRoundtrip0() throws IOException { + Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.append(new Rectangle2D.Double(0, 0, 1, .5), false); + path.append(new Rectangle2D.Double(.25, .25, .5, .5), false); + + byte[] bytes = new AdobePathWriter(path).writePath(); + Path2D readPath = new AdobePathReader(new ByteArrayImageInputStream(bytes)).readPath(); + + assertEquals(path.getWindingRule(), readPath.getWindingRule()); + assertEquals(path.getBounds2D(), readPath.getBounds2D()); + + // TODO: Would be nice, but hard to do, as we convert all points to cubic... +// assertPathEquals(path, readPath); + } + + @Test + public void testRoundtrip1() 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 AdobePathReader(data).readPath(); + byte[] bytes = new AdobePathWriter(path).writePath(); + + Path2D readPath = new AdobePathReader(new ByteArrayImageInputStream(bytes)).readPath(); + assertEquals(path.getWindingRule(), readPath.getWindingRule()); + assertEquals(path.getBounds2D(), readPath.getBounds2D()); + + assertPathEquals(path, readPath); + + assertEquals(data.length, bytes.length); + + // TODO: We currently write all points as linked, this is probably wrong + // Also... Photoshop does write "something" undocumented in the filler bytes for the length records, which may or may not be important... +// assertEquals(formatSegments(data), formatSegments(bytes)); +// assertArrayEquals(data, bytes); + } + + static String formatSegments(byte[] data) { + StringBuilder builder = new StringBuilder(data.length * 5); + + for (int i = 0; i < data.length; i += 26) { + builder.append(Arrays.toString(Arrays.copyOfRange(data, i, i + 26))).append('\n'); + } + + return builder.toString(); + } + + @Test + public void testRoundtrip2() 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 AdobePathReader(data).readPath(); + byte[] bytes = new AdobePathWriter(path).writePath(); + + Path2D readPath = new AdobePathReader(new ByteArrayImageInputStream(bytes)).readPath(); + assertEquals(path.getWindingRule(), readPath.getWindingRule()); + assertEquals(path.getBounds2D(), readPath.getBounds2D()); + + assertPathEquals(path, readPath); + + assertEquals(data.length, bytes.length); + + // TODO: We currently write all points as linked, this is probably wrong + // Also... Photoshop does write "something" undocumented in the filler bytes for the length records, which may or may not be important... +// assertEquals(formatSegments(data), formatSegments(bytes)); +// assertArrayEquals(data, bytes); + } +} 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 index 7aca08ee..e40e034b 100644 --- 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 @@ -125,12 +125,12 @@ public class PathsTest { } @Test(expected = IllegalArgumentException.class) - public void testApplyClippingPathNullPath() throws IOException { + public void testApplyClippingPathNullPath() { Paths.applyClippingPath(null, new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY)); } @Test(expected = IllegalArgumentException.class) - public void testApplyClippingPathNullSource() throws IOException { + public void testApplyClippingPathNullSource() { Paths.applyClippingPath(new GeneralPath(), null); } @@ -147,7 +147,7 @@ public class PathsTest { assertEquals(source.getWidth(), image.getWidth()); assertEquals(source.getHeight(), image.getHeight()); // Transparent - assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT); + assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency()); // Corners (at least) should be transparent assertEquals(0, image.getRGB(0, 0)); @@ -161,8 +161,9 @@ public class PathsTest { // TODO: Mor sophisticated test that tests all pixels outside path... } + @SuppressWarnings("ConstantConditions") @Test(expected = IllegalArgumentException.class) - public void testApplyClippingPathNullDestination() throws IOException { + public void testApplyClippingPathNullDestination() { Paths.applyClippingPath(new GeneralPath(), new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), null); } @@ -209,7 +210,7 @@ public class PathsTest { assertEquals(857, image.getWidth()); assertEquals(1800, image.getHeight()); // Transparent - assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT); + assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency()); // Corners (at least) should be transparent assertEquals(0, image.getRGB(0, 0)); @@ -230,34 +231,34 @@ public class PathsTest { } static Path2D readExpectedPath(final String resource) throws IOException { - ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource)); - - try { + try (ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource))) { 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()) { + while(!expectedIterator.isDone()) { + assertFalse("Less points than expected", actualIterator.isDone()); + int expectedType = expectedIterator.currentSegment(expectedCoords); int actualType = actualIterator.currentSegment(actualCoords); - assertEquals(expectedType, actualType); - assertArrayEquals(expectedCoords, actualCoords, 0); + assertEquals("Unexpected segment type", expectedType, actualType); + assertArrayEquals("Unexpected coordinates", expectedCoords, actualCoords, 0); actualIterator.next(); expectedIterator.next(); } + + assertTrue("More points than expected", actualIterator.isDone()); } } diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java index b20ace02..2c8f25db 100755 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java @@ -83,7 +83,6 @@ public final class PSDReader extends MetadataReader { PSDResource resource = new PSDResource(id, input); entries.add(new PSDEntry(id, resource.name(), resource.data())); - } catch (EOFException e) { break; From 5c1f51f3cab50c6dfcbdac4d090fbfbce6222a92 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 9 Jan 2020 21:23:20 +0100 Subject: [PATCH 06/17] #490: Fixed roundtrip tests and tuned collinearity threshold. --- .../imageio/path/AdobePathWriter.java | 35 ++++++------------- .../imageio/path/AdobePathWriterTest.java | 31 +++++++++++----- 2 files changed, 33 insertions(+), 33 deletions(-) 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 index 735ffee7..26f52068 100755 --- 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 @@ -53,6 +53,9 @@ import static com.twelvemonkeys.lang.Validate.notNull; */ public final class AdobePathWriter { + // TODO: Might need to get hold of more real Photoshop samples to tune this threshold... + private static final double COLLINEARITY_THRESHOLD = 0.00000001; + private final List segments; /** @@ -97,9 +100,8 @@ public final class AdobePathWriter { System.err.println("coords: " + Arrays.toString(coords)); } - // TODO: We need to support unlinked segments! - - boolean collinear; + // We write collinear points as linked segments + boolean collinear = isCollinear(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); switch (segmentType) { case PathIterator.SEG_MOVETO: @@ -110,22 +112,16 @@ public final class AdobePathWriter { break; case PathIterator.SEG_LINETO: - collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); - System.out.println("isCollinear? " + collinear); subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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: - collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); - System.out.println("isCollinear? " + collinear); subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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: - collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, prev.apx, prev.apy, coords[0], coords[1]); - System.out.println("isCollinear? " + collinear); subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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; @@ -139,8 +135,7 @@ public final class AdobePathWriter { throw new AssertionError("Not a closed path"); } - collinear = isCollinearAndSameDistance(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.cplx, initial.cply); - System.out.println("isCollinear? " + collinear); + collinear = isCollinear(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.cplx, initial.cply); subpath.set(0, new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, prev.cppy, prev.cppx, initial.apy, initial.apx, initial.cply, initial.cplx)); // Add to full path @@ -158,20 +153,12 @@ public final class AdobePathWriter { return segments; } - private static final double COLLINEARITY_THRESHOLD = 0.035; - - private static boolean isCollinearAndSameDistance(double x1, double y1, double x2, double y2, double x3, double y3) { -// return (y3 - y2) * (x2 - x1) == (y2 - y1) * (x3 - x2); // Collinear Slope -// return x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2) == 0; // Collinear (Double) Area - -// return Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) <= 0.0005; // With some slack... - -// return Math.abs(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) - Math.sqrt(Math.pow(x3 - x2, 2) + Math.pow(y3 - y2, 2))) <= 0.01; - - // TODO: Get hold of a real Photoshop sample... The current data may be wrong. - // TODO: If correct, PS writes linked if all points are the same... + private static boolean isCollinear(double x1, double y1, double x2, double y2, double x3, double y3) { + // PS seems to write as linked if all points are the same.... return (x1 == x2 && x2 == x3 && y1 == y2 && y2 == y3) || - (x1 != x2 || y1 != y2) && (x2 != x3 || y2 != y3) && Math.abs((x2 - x1) - (x3 - x2)) <= COLLINEARITY_THRESHOLD && Math.abs((y2 - y1) - (y3 - y2)) <= COLLINEARITY_THRESHOLD; + (x1 != x2 || y1 != y2) && (x2 != x3 || y2 != y3) && + Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) <= COLLINEARITY_THRESHOLD; // With some slack... + } void writePathResource(final DataOutput output) throws IOException { diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java index f46d02c2..f972f906 100644 --- a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java +++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java @@ -245,13 +245,25 @@ public class AdobePathWriterTest { assertEquals(data.length, bytes.length); - // TODO: We currently write all points as linked, this is probably wrong - // Also... Photoshop does write "something" undocumented in the filler bytes for the length records, which may or may not be important... -// assertEquals(formatSegments(data), formatSegments(bytes)); -// assertArrayEquals(data, bytes); + // Path segment 3 contains some unknown bits in the filler bytes, we'll ignore those... + cleanLengthRecords(data); + + assertEquals(formatSegments(data), formatSegments(bytes)); + assertArrayEquals(data, bytes); } - static String formatSegments(byte[] data) { + private static void cleanLengthRecords(byte[] data) { + for (int i = 0; i < data.length; i += 26) { + if (data[i + 1] == CLOSED_SUBPATH_LENGTH_RECORD) { + // Clean everything after record type and length field + for (int j = 4; j < 26; j++) { + data[i + j] = 0; + } + } + } + } + + private static String formatSegments(byte[] data) { StringBuilder builder = new StringBuilder(data.length * 5); for (int i = 0; i < data.length; i += 26) { @@ -282,9 +294,10 @@ public class AdobePathWriterTest { assertEquals(data.length, bytes.length); - // TODO: We currently write all points as linked, this is probably wrong - // Also... Photoshop does write "something" undocumented in the filler bytes for the length records, which may or may not be important... -// assertEquals(formatSegments(data), formatSegments(bytes)); -// assertArrayEquals(data, bytes); + // Path segment 3 and 48 contains some unknown bits in the filler bytes, we'll ignore that: + cleanLengthRecords(data); + + assertEquals(formatSegments(data), formatSegments(bytes)); + assertArrayEquals(data, bytes); } } From d3249dc3d599e7086e543e036f7372df98834fe2 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 9 Jan 2020 21:25:41 +0100 Subject: [PATCH 07/17] #490: Refactorings. --- .../imageio/path/AdobePathReader.java | 16 ++++++---------- .../imageio/path/AdobePathSegment.java | 8 ++++++++ .../imageio/path/AdobePathWriter.java | 4 ---- 3 files changed, 14 insertions(+), 14 deletions(-) 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 index cae9d8dd..fefc07c5 100755 --- 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 @@ -230,18 +230,14 @@ public final class AdobePathReader { default: return new AdobePathSegment( selector, - toFixedPoint(data.readInt()), - toFixedPoint(data.readInt()), - toFixedPoint(data.readInt()), - toFixedPoint(data.readInt()), - toFixedPoint(data.readInt()), - toFixedPoint(data.readInt()) + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(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 index d5cc82cf..739e219d 100755 --- 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 @@ -131,6 +131,14 @@ final class AdobePathSegment { this.cplx = cplx; } + static int toFixedPoint(final double value) { + return (int) Math.round(value * 0x1000000); + } + + static double fromFixedPoint(final int fixed) { + return ((double) fixed / 0x1000000); + } + @Override public boolean equals(final Object other) { if (this == other) { 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 index 26f52068..89be1594 100755 --- 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 @@ -219,8 +219,4 @@ public final class AdobePathWriter { return bytes.toByteArray(); } - // TODO: Move to AdobePathSegment - private static int toFixedPoint(final double value) { - return (int) Math.round(value * 0x1000000); - } } From 167686bdea303eff88558eff37e0676c60179406 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 9 Jan 2020 21:28:37 +0100 Subject: [PATCH 08/17] #490: Minor debug cleanup. --- .../com/twelvemonkeys/imageio/path/AdobePathWriter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 89be1594..d387d44f 100755 --- 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 @@ -97,7 +97,7 @@ public final class AdobePathWriter { if (DEBUG) { System.out.println("segmentType: " + segmentType); - System.err.println("coords: " + Arrays.toString(coords)); + System.out.println("coords: " + Arrays.toString(coords)); } // We write collinear points as linked segments @@ -172,8 +172,8 @@ public final class AdobePathWriter { public void writePath(final DataOutput output) throws IOException { if (DEBUG) { - System.err.println("segments: " + segments.size()); - System.err.println(segments); + System.out.println("segments: " + segments.size()); + System.out.println(segments); } for (AdobePathSegment segment : segments) { From e5c6832ec0acd03a041b658b58682afa47669894 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 13 Jan 2020 21:03:40 +0100 Subject: [PATCH 09/17] #490: Allow writing more TIFF fields. --- .../imageio/metadata/tiff/TIFF.java | 1 + .../imageio/plugins/tiff/TIFFImageWriter.java | 274 +++++++++--------- .../plugins/tiff/TIFFImageWriterTest.java | 6 +- 3 files changed, 143 insertions(+), 138 deletions(-) diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java index dd85dc88..19171d88 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java @@ -144,6 +144,7 @@ public interface TIFF { int TAG_ROWS_PER_STRIP = 278; int TAG_STRIP_BYTE_COUNTS = 279; int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange." + int TAG_FREE_BYTE_COUNTS = 289; // "Old-style" JPEG (still used as EXIF thumbnail) int TAG_JPEG_INTERCHANGE_FORMAT = 513; int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index d5f71b39..78189058 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -142,13 +142,17 @@ public final class TIFFImageWriter extends ImageWriterBase { private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset) throws IOException { RenderedImage renderedImage = image.getRenderedImage(); - - TIFFImageMetadata metadata = image.getMetadata() != null - ? convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param) - : getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(renderedImage), param); - - ColorModel colorModel = renderedImage.getColorModel(); SampleModel sampleModel = renderedImage.getSampleModel(); + + // Can't use createFromRenderedImage in this case, as it does not consider palette for TYPE_BYTE_BINARY... + // TODO: Consider writing workaround in ImageTypeSpecifiers + ImageTypeSpecifier spec = new ImageTypeSpecifier(renderedImage); + + // TODO: Handle case where convertImageMetadata returns null, due to unknown metadata format, or reconsider if that's a valid case... + TIFFImageMetadata metadata = image.getMetadata() != null + ? convertImageMetadata(image.getMetadata(), spec, param) + : getDefaultImageMetadata(spec, param); + int numBands = sampleModel.getNumBands(); int pixelSize = computePixelSize(sampleModel); @@ -170,145 +174,29 @@ public final class TIFFImageWriter extends ImageWriterBase { throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel); } - // TODO: There shouldn't be necessary to create a separate map here, this should be handled in the - // convertImageMetadata/getDefaultImageMetadata methods.... Map entries = new LinkedHashMap<>(); + // Copy metadata to output + Directory metadataIFD = metadata.getIFD(); + for (Entry entry : metadataIFD) { + entries.put((Integer) entry.getIdentifier(), entry); + } + entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth())); entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight())); - entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional) - entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize()))); - - // If numComponents > numColorComponents, write ExtraSamples - if (numBands > colorModel.getNumColorComponents()) { - // TODO: Write per component > numColorComponents - if (colorModel.hasAlpha()) { - entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)); - } - else { - entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); - } - } - - // Write compression field from param or metadata - int compression; - if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA) - && image.getMetadata() != null && metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION) != null) { - compression = ((Number) metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION).getValue()).intValue(); - } - else { - compression = TIFFImageWriteParam.getCompressionType(param); - } - - entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); - - // TODO: Let param/metadata control predictor - // TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT - switch (compression) { - case TIFFExtension.COMPRESSION_ZLIB: - case TIFFExtension.COMPRESSION_DEFLATE: - case TIFFExtension.COMPRESSION_LZW: - if (pixelSize >= 8) { - entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); - } - - break; - - case TIFFExtension.COMPRESSION_CCITT_T4: - Entry group3options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP3OPTIONS); - - if (group3options == null) { - group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING); - } - - entries.put(TIFF.TAG_GROUP3OPTIONS, group3options); - - break; - - case TIFFExtension.COMPRESSION_CCITT_T6: - Entry group4options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP4OPTIONS); - - if (group4options == null) { - group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L); - } - - entries.put(TIFF.TAG_GROUP4OPTIONS, group4options); - - break; - - default: - } - - int photometric = getPhotometricInterpretation(colorModel, compression); - entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric)); - - if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) { - // TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()... - // We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too - entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0)))); - entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); - } - else { - entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands)); - - // Note: Assuming sRGB to be the default RGB interpretation - ColorSpace colorSpace = colorModel.getColorSpace(); - if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) { - entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); - } - } - - // Default sample format SAMPLEFORMAT_UINT need not be written - if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { - entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); - } - // TODO: Float values! - - // TODO: Again, this should be handled in the metadata conversion.... - // Get Software from metadata, or use default - Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE); - entries.put(TIFF.TAG_SOFTWARE, software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion())); - - // Copy metadata to output - int[] copyTags = { - TIFF.TAG_ORIENTATION, - TIFF.TAG_DATE_TIME, - TIFF.TAG_DOCUMENT_NAME, - TIFF.TAG_IMAGE_DESCRIPTION, - TIFF.TAG_MAKE, - TIFF.TAG_MODEL, - TIFF.TAG_PAGE_NAME, - TIFF.TAG_PAGE_NUMBER, - TIFF.TAG_ARTIST, - TIFF.TAG_HOST_COMPUTER, - TIFF.TAG_COPYRIGHT - }; - for (int tagID : copyTags) { - Entry entry = metadata.getIFD().getEntryById(tagID); - if (entry != null) { - entries.put(tagID, entry); - } - } - - // Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults - // TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent. - Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION); - entries.put(TIFF.TAG_X_RESOLUTION, xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI)); - Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION); - entries.put(TIFF.TAG_Y_RESOLUTION, yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI)); - Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT); - entries.put(TIFF.TAG_RESOLUTION_UNIT, resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI)); // TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, renderedImage.getHeight())); - // - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?) + // StripByteCounts - for no compression, entire image data... entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1)); // Updated later - // - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???) + // StripOffsets - can be offset to single strip only entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1)); // Updated later // TODO: If tiled, write tile indexes etc // Depending on param.getTilingMode long nextIFDPointerOffset = -1; + int compression = ((Number) entries.get(TIFF.TAG_COMPRESSION).getValue()).intValue(); + if (compression == TIFFBaseline.COMPRESSION_NONE) { // This implementation, allows semi-streaming-compatible uncompressed TIFFs long streamPosition = imageOutput.getStreamPosition(); @@ -876,12 +764,58 @@ public final class TIFFImageWriter extends ImageWriterBase { Map entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20); + // Set software as default, may be overwritten + entries.put(TIFF.TAG_SOFTWARE, new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion())); + entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional) + if (ifd != null) { for (Entry entry : ifd) { - entries.put((Integer) entry.getIdentifier(), entry); + int tagId = (Integer) entry.getIdentifier(); + + switch (tagId) { + // Baseline + case TIFF.TAG_SUBFILE_TYPE: + case TIFF.TAG_OLD_SUBFILE_TYPE: + case TIFF.TAG_IMAGE_DESCRIPTION: + case TIFF.TAG_MAKE: + case TIFF.TAG_MODEL: + case TIFF.TAG_ORIENTATION: + case TIFF.TAG_X_RESOLUTION: + case TIFF.TAG_Y_RESOLUTION: + case TIFF.TAG_RESOLUTION_UNIT: + case TIFF.TAG_SOFTWARE: + case TIFF.TAG_DATE_TIME: + case TIFF.TAG_ARTIST: + case TIFF.TAG_HOST_COMPUTER: + case TIFF.TAG_COPYRIGHT: + // Extension + case TIFF.TAG_DOCUMENT_NAME: + case TIFF.TAG_PAGE_NAME: + case TIFF.TAG_X_POSITION: + case TIFF.TAG_Y_POSITION: + case TIFF.TAG_PAGE_NUMBER: + case TIFF.TAG_XMP: + // Private/Custom + case TIFF.TAG_IPTC: + case TIFF.TAG_PHOTOSHOP: + case TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA: + case TIFF.TAG_PHOTOSHOP_ANNOTATIONS: + case TIFF.TAG_EXIF_IFD: + case TIFF.TAG_GPS_IFD: + case TIFF.TAG_INTEROP_IFD: + entries.put(tagId, entry); + } } } + ColorModel colorModel = imageType.getColorModel(); + SampleModel sampleModel = imageType.getSampleModel(); + int numBands = sampleModel.getNumBands(); + int pixelSize = computePixelSize(sampleModel); + + entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize()))); + + // Compression field from param or metadata int compression; if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA) && ifd != null && ifd.getEntryById(TIFF.TAG_COMPRESSION) != null) { @@ -890,11 +824,81 @@ public final class TIFFImageWriter extends ImageWriterBase { else { compression = TIFFImageWriteParam.getCompressionType(param); } + entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression)); - int photometricInterpretation = getPhotometricInterpretation(imageType.getColorModel(), compression); + // TODO: Allow metadata to take precedence? + int photometricInterpretation = getPhotometricInterpretation(colorModel, compression); entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, photometricInterpretation)); - // TODO: Set values from param if != null + combined values... + // If numComponents > numColorComponents, write ExtraSamples + if (numBands > colorModel.getNumColorComponents()) { + // TODO: Write per component > numColorComponents + if (colorModel.hasAlpha()) { + entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA)); + } + else { + entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED)); + } + } + + switch (compression) { + case TIFFExtension.COMPRESSION_ZLIB: + case TIFFExtension.COMPRESSION_DEFLATE: + case TIFFExtension.COMPRESSION_LZW: + // TODO: Let param/metadata control predictor + // TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT + if (pixelSize >= 8) { + entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING)); + } + + break; + + case TIFFExtension.COMPRESSION_CCITT_T4: + Entry group3options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP3OPTIONS) : null; + + if (group3options == null) { + group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING); + } + + entries.put(TIFF.TAG_GROUP3OPTIONS, group3options); + + break; + + case TIFFExtension.COMPRESSION_CCITT_T6: + Entry group4options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP4OPTIONS) : null; + + if (group4options == null) { + group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L); + } + + entries.put(TIFF.TAG_GROUP4OPTIONS, group4options); + + break; + + default: + } + + if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) { + // TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()... + // We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too + entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0)))); + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1)); + } + else { + entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands)); + + // Note: Assuming sRGB to be the default RGB interpretation + ColorSpace colorSpace = colorModel.getColorSpace(); + if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) { + entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); + } + } + + // Default sample format SAMPLEFORMAT_UINT need not be written + if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { + entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT)); + } + // TODO: Float values! return new TIFFImageMetadata(entries.values()); } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java index 164ad217..9f971437 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java @@ -616,7 +616,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest { int maxH = Math.min(300, image.getHeight()); for (int y = 0; y < maxH; y++) { for (int x = 0; x < image.getWidth(); x++) { - assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0); + assertRGBEquals(String.format("Pixel differ: @%d,%d", x, y), orig.getRGB(x, y), image.getRGB(x, y), 0); } } @@ -654,7 +654,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest { assumeNotNull(original); - // Write it back, using same compression (copied from metadata) + // Write it back, using deflate compression FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { @@ -718,7 +718,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest { assumeNotNull(original); - // Write it back, using same compression (copied from metadata) + // Write it back, no compression FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { From 278ce6ef33a7e113917afb9d0a29346946dfda66 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 16 Jan 2020 19:31:54 +0100 Subject: [PATCH 10/17] #490: Now allows writing paths in TIFF and JPEG. --- .../imageio/path/AdobePathWriter.java | 40 +++++-- .../com/twelvemonkeys/imageio/path/Paths.java | 110 +++++++++++++++++- 2 files changed, 138 insertions(+), 12 deletions(-) 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 index d387d44f..921fe418 100755 --- 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 @@ -67,16 +67,14 @@ public final class AdobePathWriter { * regardless of image dimensions. *

* - * @param path A {@code Path2D} instance that has {@link Path2D#WIND_EVEN_ODD WIND_EVEN_ODD} rule + * @param path A {@code Shape} 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) { + public AdobePathWriter(final Shape 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)); @@ -84,6 +82,9 @@ public final class AdobePathWriter { // 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) { + // TODO: Test if PS really ignores winding rule as documented... Otherwise we could support writing non-zero too. + isTrue(pathIterator.getWindingRule() == Path2D.WIND_EVEN_ODD, pathIterator.getWindingRule(), "Only even/odd winding rule supported: %d"); + double[] coords = new double[6]; AdobePathSegment prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 0, 0, 0, 0); @@ -154,13 +155,19 @@ public final class AdobePathWriter { } private static boolean isCollinear(double x1, double y1, double x2, double y2, double x3, double y3) { - // PS seems to write as linked if all points are the same.... + // Photoshop seems to write as linked if all points are the same.... return (x1 == x2 && x2 == x3 && y1 == y2 && y2 == y3) || (x1 != x2 || y1 != y2) && (x2 != x3 || y2 != y3) && Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) <= COLLINEARITY_THRESHOLD; // With some slack... } + /** + * Writes the path as a complete Photoshop clipping path resource to the given stream. + * + * @param output the stream to write to. + * @throws IOException if an I/O exception happens during writing. + */ void writePathResource(final DataOutput output) throws IOException { output.writeInt(PSD.RESOURCE_TYPE); output.writeShort(PSD.RES_CLIPPING_PATH); @@ -170,6 +177,12 @@ public final class AdobePathWriter { writePath(output); } + /** + * Writes the path as a set of Adobe path segments to the given stream. + * + * @param output the stream to write to. + * @throws IOException if an I/O exception happens during writing. + */ public void writePath(final DataOutput output) throws IOException { if (DEBUG) { System.out.println("segments: " + segments.size()); @@ -204,16 +217,29 @@ public final class AdobePathWriter { } } + // TODO: Do we need to care about endianness for TIFF files? // TODO: Better name? + byte[] writePathResource() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try (DataOutputStream stream = new DataOutputStream(bytes)) { + writePathResource(stream); + } + catch (IOException e) { + throw new AssertionError("ByteArrayOutputStream threw IOException", e); + } + + return bytes.toByteArray(); + } + public byte[] writePath() { - // 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); + throw new AssertionError("ByteArrayOutputStream threw IOException", e); } return bytes.toByteArray(); 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 index 7cf3e33f..79efcbed 100755 --- 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 @@ -43,8 +43,11 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; -import javax.imageio.ImageIO; +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.MemoryCacheImageInputStream; import java.awt.*; import java.awt.geom.AffineTransform; @@ -52,13 +55,15 @@ import java.awt.geom.Path2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.util.LinkedHashMap; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; import java.util.List; import java.util.Map; import static com.twelvemonkeys.lang.Validate.isTrue; import static com.twelvemonkeys.lang.Validate.notNull; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; /** * Support for various Adobe Photoshop Path related operations: @@ -119,9 +124,7 @@ public final class Paths { } else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) { // JPEG version - Map> segmentIdentifiers = new LinkedHashMap<>(); - segmentIdentifiers.put(JPEG.APP13, singletonList("Photoshop 3.0")); - + Map> segmentIdentifiers = singletonMap(JPEG.APP13, singletonList("Photoshop 3.0")); List photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers); if (!photoshop.isEmpty()) { @@ -254,6 +257,103 @@ public final class Paths { return applyClippingPath(clip, image); } + public static boolean writeClipped(final BufferedImage image, Shape clipPath, final String formatName, final ImageOutputStream output) throws IOException { + if (image == null) { + throw new IllegalArgumentException("image == null!"); + } + if (formatName == null) { + throw new IllegalArgumentException("formatName == null!"); + } + if (output == null) { + throw new IllegalArgumentException("output == null!"); + } + + String format = "JPG".equalsIgnoreCase(formatName) ? "JPEG" : formatName.toUpperCase(); + + if ("TIFF".equals(format) || "JPEG".equals(format)) { + ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image); + Iterator writers = ImageIO.getImageWriters(type, formatName); + + if (writers.hasNext()) { + ImageWriter writer = writers.next(); + + ImageWriteParam param = writer.getDefaultWriteParam(); + IIOMetadata metadata = writer.getDefaultImageMetadata(type, param); + + byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(); + + if ("TIFF".equals(format)) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("Deflate"); + + String metadataFormat = "com_sun_media_imageio_plugins_tiff_image_1.0"; + IIOMetadataNode root = new IIOMetadataNode(metadataFormat); + IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); + + IIOMetadataNode pathField = new IIOMetadataNode("TIFFField"); + pathField.setAttribute("number", String.valueOf(TIFF.TAG_PHOTOSHOP)); + IIOMetadataNode pathValue = new IIOMetadataNode("TIFFUndefined"); // Use undefined for simplicity, could also use bytes + pathValue.setAttribute("value", arrayAsString(pathResource)); + + pathField.appendChild(pathValue); + ifd.appendChild(pathField); + root.appendChild(ifd); + + metadata.mergeTree(metadataFormat, root); + } + else if ("JPEG".equals(format)) { + String metadataFormat = "javax_imageio_jpeg_image_1.0"; + IIOMetadataNode root = new IIOMetadataNode(metadataFormat); + + root.appendChild(new IIOMetadataNode("JPEGvariety")); + + IIOMetadataNode sequence = new IIOMetadataNode("markerSequence"); + + // App13/Photshop 3.0 + IIOMetadataNode unknown = new IIOMetadataNode("unknown"); + unknown.setAttribute("MarkerTag", Integer.toString(JPEG.APP13 & 0xFF)); + + byte[] identfier = "Photoshop 3.0".getBytes(StandardCharsets.US_ASCII); + byte[] data = new byte[identfier.length + 1 + pathResource.length]; + System.arraycopy(identfier, 0, data, 0, identfier.length); + System.arraycopy(pathResource, 0, data, identfier.length + 1, pathResource.length); + + unknown.setUserObject(data); + + sequence.appendChild(unknown); + root.appendChild(sequence); + + metadata.mergeTree(metadataFormat, root); + } + // TODO: Else if PSD... Requires PSD write + new metadata format... + + writer.setOutput(output); + writer.write(null, new IIOImage(image, null, metadata), param); + + return true; + } + } + + return false; + } + + private static String arrayAsString(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; ; i++) { + builder.append(bytes[i]); + + if (i == bytes.length - 1) { + return builder.toString(); + } + + builder.append(", "); + } + } + // Test code public static void main(final String[] args) throws IOException, InterruptedException { BufferedImage destination; From d2b58ed20e3add2d88d4a39c8dd1e1184f1be70e Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 17 Jan 2020 16:43:27 +0100 Subject: [PATCH 11/17] #490: Now allows writing using standard TIFF writer in Java 9+ --- .../main/java/com/twelvemonkeys/imageio/path/Paths.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 index 79efcbed..4562dd67 100755 --- 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 @@ -62,6 +62,7 @@ import java.util.Map; import static com.twelvemonkeys.lang.Validate.isTrue; import static com.twelvemonkeys.lang.Validate.notNull; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; @@ -279,6 +280,7 @@ public final class Paths { ImageWriteParam param = writer.getDefaultWriteParam(); IIOMetadata metadata = writer.getDefaultImageMetadata(type, param); + List metadataFormats = asList(metadata.getMetadataFormatNames()); byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(); @@ -286,7 +288,10 @@ public final class Paths { param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionType("Deflate"); - String metadataFormat = "com_sun_media_imageio_plugins_tiff_image_1.0"; + // Check if the format is that of the bundled TIFF writer, otherwise use JAI format + String metadataFormat = metadataFormats.contains("javax_imageio_tiff_image_1.0") + ? "javax_imageio_tiff_image_1.0" + : "com_sun_media_imageio_plugins_tiff_image_1.0"; // Fails in mergeTree, if not supported IIOMetadataNode root = new IIOMetadataNode(metadataFormat); IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); @@ -350,7 +355,7 @@ public final class Paths { return builder.toString(); } - builder.append(", "); + builder.append(","); // NOTE: The javax_imageio_tiff_image_1.0 format does not allow whitespace here... } } From 420f78be88a1e4a5c6de551994f181edfa9f86d0 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Fri, 17 Jan 2020 16:44:42 +0100 Subject: [PATCH 12/17] #490: Fix for "incomplete" paths with implicit line back to start. --- .../imageio/path/AdobePathWriter.java | 10 +++-- .../imageio/path/AdobePathWriterTest.java | 45 +++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) 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 index 921fe418..493305f8 100755 --- 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 @@ -131,9 +131,10 @@ public final class AdobePathWriter { // 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)); - throw new AssertionError("Not a closed path"); + // Line back to initial if last anchor point does not equal initial anchor + collinear = isCollinear(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.apx, initial.apy); + subpath.add(new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, prev.cppy, prev.cppx, prev.apy, prev.apx, initial.apy, initial.apx)); + prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, initial.apy, initial.apx, initial.apy, initial.apx, 0, 0); } collinear = isCollinear(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.cplx, initial.cply); @@ -151,6 +152,9 @@ public final class AdobePathWriter { pathIterator.next(); } + // TODO: If subpath is not empty at this point, there was no close segment... + // Either wrap up (if coordinates match), or throw exception (otherwise) + return segments; } diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java index f972f906..ad515e33 100644 --- a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java +++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java @@ -44,8 +44,7 @@ import java.util.Arrays; import static com.twelvemonkeys.imageio.path.AdobePathSegment.*; import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * AdobePathWriterTest. @@ -93,6 +92,46 @@ public class AdobePathWriterTest { new AdobePathWriter(path); } + @Test + public void testCreateClosed() { + GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.moveTo(.5, .5); + path.lineTo(1, .5); + path.curveTo(1, 1, 1, 1, .5, 1); + path.closePath(); + + new AdobePathWriter(path).writePath(); + + fail("Test that we have 4 segments"); + } + + @Test + public void testCreateImplicitClosed() { + GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.moveTo(.5, .5); + path.lineTo(1, .5); + path.curveTo(1, 1, 1, 1, .5, 1); + path.lineTo(.5, .5); + + new AdobePathWriter(path).writePath(); // TODO: Should we allow this? + + fail("Test that we have 4 segments, and that it is equal to the one above"); + } + + @Test + public void testCreateDoubleClosed() { + GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.moveTo(.5, .5); + path.lineTo(1, .5); + path.curveTo(1, 1, 1, 1, .5, 1); + path.lineTo(.5, .5); + path.closePath(); + + new AdobePathWriter(path).writePath(); + + fail("Test that we have 4 segments, and that it is equal to the one above"); + } + @Test public void testWriteToStream() throws IOException { Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD); @@ -168,7 +207,7 @@ public class AdobePathWriterTest { AdobePathWriter pathCreator = new AdobePathWriter(path); byte[] bytes = pathCreator.writePath(); - System.err.println(Arrays.toString(bytes)); +// System.err.println(Arrays.toString(bytes)); assertEquals(12 * 26, bytes.length); From 8b86b57e63db7f59919b8185334678ea1f295f0d Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 22 Jan 2020 20:06:30 +0100 Subject: [PATCH 13/17] #490: Fix for "incomplete" paths with implicit line back to start. --- .../imageio/path/AdobePathWriter.java | 41 ++++-- .../com/twelvemonkeys/imageio/path/Paths.java | 135 +++++++++++------- .../imageio/path/AdobePathWriterTest.java | 90 ++++++++++-- .../twelvemonkeys/imageio/path/PathsTest.java | 39 +++++ 4 files changed, 228 insertions(+), 77 deletions(-) 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 index 493305f8..f3f01baf 100755 --- 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 @@ -67,11 +67,12 @@ public final class AdobePathWriter { * regardless of image dimensions. *

* - * @param path A {@code Shape} 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]. + * @param path A {@code Shape} instance that has {@link Path2D#WIND_EVEN_ODD WIND_EVEN_ODD} rule, + * is contained within the rectangle [x=0.0,y=0.0,w=1.0,h=1.0], and is closed. * @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]. + * the paths bounding box is outside [x=0.0,y=0.0,w=1.0,h=1.0] or + * the path is not closed. */ public AdobePathWriter(final Shape path) { notNull(path, "path"); @@ -128,8 +129,8 @@ public final class AdobePathWriter { break; case PathIterator.SEG_CLOSE: - // Replace initial point. AdobePathSegment initial = subpath.get(0); + if (initial.apx != prev.apx || initial.apy != prev.apy) { // Line back to initial if last anchor point does not equal initial anchor collinear = isCollinear(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.apx, initial.apy); @@ -137,13 +138,7 @@ public final class AdobePathWriter { prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, initial.apy, initial.apx, initial.apy, initial.apx, 0, 0); } - collinear = isCollinear(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.cplx, initial.cply); - subpath.set(0, new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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); - + close(initial, prev, subpath, segments); subpath.clear(); break; @@ -152,12 +147,31 @@ public final class AdobePathWriter { pathIterator.next(); } - // TODO: If subpath is not empty at this point, there was no close segment... - // Either wrap up (if coordinates match), or throw exception (otherwise) + // If subpath is not empty at this point, there was no close segment... + // Wrap up if coordinates match, otherwise throw exception + if (!subpath.isEmpty()) { + AdobePathSegment initial = subpath.get(0); + + if (initial.apx != prev.apx || initial.apy != prev.apy) { + throw new IllegalArgumentException("Path must be closed"); + } + + close(initial, prev, subpath, segments); + } return segments; } + private static void close(AdobePathSegment initial, AdobePathSegment prev, List subpath, List segments) { + // Replace initial point. + boolean collinear = isCollinear(prev.cppx, prev.cppy, initial.apx, initial.apy, initial.cplx, initial.cply); + subpath.set(0, new AdobePathSegment(collinear ? CLOSED_SUBPATH_BEZIER_LINKED : CLOSED_SUBPATH_BEZIER_UNLINKED, 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); + } + private static boolean isCollinear(double x1, double y1, double x2, double y2, double x3, double y3) { // Photoshop seems to write as linked if all points are the same.... return (x1 == x2 && x2 == x3 && y1 == y2 && y2 == y3) || @@ -248,5 +262,4 @@ public final class AdobePathWriter { return bytes.toByteArray(); } - } 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 index 4562dd67..ce95a4f8 100755 --- 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 @@ -53,6 +53,7 @@ import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -258,7 +259,30 @@ public final class Paths { return applyClippingPath(clip, image); } - public static boolean writeClipped(final BufferedImage image, Shape clipPath, final String formatName, final ImageOutputStream output) throws IOException { + /** + * Writes the image along with a clipping path resource, in the given format to the supplied output. + * The image is written to the + * {@code ImageOutputStream} starting at the current stream + * pointer, overwriting existing stream data from that point + * forward, if present. + *

+ * Note: As {@link ImageIO#write(RenderedImage, String, ImageOutputStream)}, this method does + * not close the output stream. + * It is the responsibility of the caller to close the stream, if desired. + *

+ * + * @param image the image to be written, may not be {@code null}. + * @param clipPath the clip path, may not be {@code null}. + * @param formatName the informal format name, may not be {@code null}. + * @param output the stream to write to, may not be {@code null}. + * + * @return {@code true} if the image was written, + * otherwise {@code false} (ie. no writer was found for the specified format). + * + * @exception IllegalArgumentException if any parameter is {@code null}. + * @exception IOException if an error occurs during writing. + */ + public static boolean writeClipped(final RenderedImage image, Shape clipPath, final String formatName, final ImageOutputStream output) throws IOException { if (image == null) { throw new IllegalArgumentException("image == null!"); } @@ -269,74 +293,75 @@ public final class Paths { throw new IllegalArgumentException("output == null!"); } - String format = "JPG".equalsIgnoreCase(formatName) ? "JPEG" : formatName.toUpperCase(); + ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image); + Iterator writers = ImageIO.getImageWriters(type, formatName); - if ("TIFF".equals(format) || "JPEG".equals(format)) { - ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image); - Iterator writers = ImageIO.getImageWriters(type, formatName); + if (writers.hasNext()) { + ImageWriter writer = writers.next(); - if (writers.hasNext()) { - ImageWriter writer = writers.next(); + ImageWriteParam param = writer.getDefaultWriteParam(); + IIOMetadata metadata = writer.getDefaultImageMetadata(type, param); + List metadataFormats = asList(metadata.getMetadataFormatNames()); - ImageWriteParam param = writer.getDefaultWriteParam(); - IIOMetadata metadata = writer.getDefaultImageMetadata(type, param); - List metadataFormats = asList(metadata.getMetadataFormatNames()); + byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(); - byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(); + if (metadataFormats.contains("javax_imageio_tiff_image_1.0") || metadataFormats.contains("com_sun_media_imageio_plugins_tiff_image_1.0")) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("Deflate"); - if ("TIFF".equals(format)) { - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionType("Deflate"); + // Check if the format is that of the bundled TIFF writer, otherwise use JAI format + String metadataFormat = metadataFormats.contains("javax_imageio_tiff_image_1.0") + ? "javax_imageio_tiff_image_1.0" + : "com_sun_media_imageio_plugins_tiff_image_1.0"; // Fails in mergeTree, if not supported + IIOMetadataNode root = new IIOMetadataNode(metadataFormat); + IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); - // Check if the format is that of the bundled TIFF writer, otherwise use JAI format - String metadataFormat = metadataFormats.contains("javax_imageio_tiff_image_1.0") - ? "javax_imageio_tiff_image_1.0" - : "com_sun_media_imageio_plugins_tiff_image_1.0"; // Fails in mergeTree, if not supported - IIOMetadataNode root = new IIOMetadataNode(metadataFormat); - IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); + IIOMetadataNode pathField = new IIOMetadataNode("TIFFField"); + pathField.setAttribute("number", String.valueOf(TIFF.TAG_PHOTOSHOP)); + IIOMetadataNode pathValue = new IIOMetadataNode("TIFFUndefined"); // Use undefined for simplicity, could also use bytes + pathValue.setAttribute("value", arrayAsString(pathResource)); - IIOMetadataNode pathField = new IIOMetadataNode("TIFFField"); - pathField.setAttribute("number", String.valueOf(TIFF.TAG_PHOTOSHOP)); - IIOMetadataNode pathValue = new IIOMetadataNode("TIFFUndefined"); // Use undefined for simplicity, could also use bytes - pathValue.setAttribute("value", arrayAsString(pathResource)); + pathField.appendChild(pathValue); + ifd.appendChild(pathField); + root.appendChild(ifd); - pathField.appendChild(pathValue); - ifd.appendChild(pathField); - root.appendChild(ifd); - - metadata.mergeTree(metadataFormat, root); - } - else if ("JPEG".equals(format)) { - String metadataFormat = "javax_imageio_jpeg_image_1.0"; - IIOMetadataNode root = new IIOMetadataNode(metadataFormat); - - root.appendChild(new IIOMetadataNode("JPEGvariety")); - - IIOMetadataNode sequence = new IIOMetadataNode("markerSequence"); - - // App13/Photshop 3.0 - IIOMetadataNode unknown = new IIOMetadataNode("unknown"); - unknown.setAttribute("MarkerTag", Integer.toString(JPEG.APP13 & 0xFF)); - - byte[] identfier = "Photoshop 3.0".getBytes(StandardCharsets.US_ASCII); - byte[] data = new byte[identfier.length + 1 + pathResource.length]; - System.arraycopy(identfier, 0, data, 0, identfier.length); - System.arraycopy(pathResource, 0, data, identfier.length + 1, pathResource.length); - - unknown.setUserObject(data); - - sequence.appendChild(unknown); - root.appendChild(sequence); - - metadata.mergeTree(metadataFormat, root); - } - // TODO: Else if PSD... Requires PSD write + new metadata format... + metadata.mergeTree(metadataFormat, root); writer.setOutput(output); writer.write(null, new IIOImage(image, null, metadata), param); return true; } + else if (metadataFormats.contains("javax_imageio_jpeg_image_1.0")) { + String metadataFormat = "javax_imageio_jpeg_image_1.0"; + IIOMetadataNode root = new IIOMetadataNode(metadataFormat); + + root.appendChild(new IIOMetadataNode("JPEGvariety")); + + IIOMetadataNode sequence = new IIOMetadataNode("markerSequence"); + + // App13/Photshop 3.0 + IIOMetadataNode unknown = new IIOMetadataNode("unknown"); + unknown.setAttribute("MarkerTag", Integer.toString(JPEG.APP13 & 0xFF)); + + byte[] identfier = "Photoshop 3.0".getBytes(StandardCharsets.US_ASCII); + byte[] data = new byte[identfier.length + 1 + pathResource.length]; + System.arraycopy(identfier, 0, data, 0, identfier.length); + System.arraycopy(pathResource, 0, data, identfier.length + 1, pathResource.length); + + unknown.setUserObject(data); + + sequence.appendChild(unknown); + root.appendChild(sequence); + + metadata.mergeTree(metadataFormat, root); + + writer.setOutput(output); + writer.write(null, new IIOImage(image, null, metadata), param); + + return true; + } + // TODO: Else if PSD... Requires PSD write + new metadata format... } return false; diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java index ad515e33..5179ed1e 100644 --- a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java +++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java @@ -44,7 +44,8 @@ import java.util.Arrays; import static com.twelvemonkeys.imageio.path.AdobePathSegment.*; import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; /** * AdobePathWriterTest. @@ -92,6 +93,16 @@ public class AdobePathWriterTest { new AdobePathWriter(path); } + @Test(expected = IllegalArgumentException.class) + public void testCreateNotClosed() { + GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD); + path.moveTo(.5, .5); + path.lineTo(1, .5); + path.curveTo(1, 1, 1, 1, .5, 1); + + new AdobePathWriter(path).writePath(); + } + @Test public void testCreateClosed() { GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD); @@ -100,9 +111,30 @@ public class AdobePathWriterTest { path.curveTo(1, 1, 1, 1, .5, 1); path.closePath(); - new AdobePathWriter(path).writePath(); + byte[] bytes = new AdobePathWriter(path).writePath(); - fail("Test that we have 4 segments"); + assertEquals(6 * 26, bytes.length); + + int off = 0; + + // Path/initial fill rule: Even-Odd (0) + assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Rectangle 1: 0, 0, 1, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Sanity + assertEquals(bytes.length, off); } @Test @@ -113,9 +145,31 @@ public class AdobePathWriterTest { path.curveTo(1, 1, 1, 1, .5, 1); path.lineTo(.5, .5); - new AdobePathWriter(path).writePath(); // TODO: Should we allow this? + byte[] bytes = new AdobePathWriter(path).writePath(); + + assertEquals(6 * 26, bytes.length); + + int off = 0; + + // Path/initial fill rule: Even-Odd (0) + assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Rectangle 1: 0, 0, 1, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Sanity + assertEquals(bytes.length, off); - fail("Test that we have 4 segments, and that it is equal to the one above"); } @Test @@ -127,9 +181,30 @@ public class AdobePathWriterTest { path.lineTo(.5, .5); path.closePath(); - new AdobePathWriter(path).writePath(); + byte[] bytes = new AdobePathWriter(path).writePath(); - fail("Test that we have 4 segments, and that it is equal to the one above"); + assertEquals(6 * 26, bytes.length); + + int off = 0; + + // Path/initial fill rule: Even-Odd (0) + assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Rectangle 1: 0, 0, 1, .5 + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0}, + Arrays.copyOfRange(bytes, off, off += 26)); + + // Sanity + assertEquals(bytes.length, off); } @Test @@ -207,7 +282,6 @@ public class AdobePathWriterTest { AdobePathWriter pathCreator = new AdobePathWriter(path); byte[] bytes = pathCreator.writePath(); -// System.err.println(Arrays.toString(bytes)); assertEquals(12 * 26, bytes.length); 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 index e40e034b..9f5b373b 100644 --- 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 @@ -38,15 +38,18 @@ import org.junit.Test; import javax.imageio.ImageIO; import javax.imageio.spi.IIORegistry; import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; 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.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; /** * PathsTest. @@ -240,6 +243,9 @@ public class PathsTest { } static void assertPathEquals(final Path2D expectedPath, final Path2D actualPath) { + assertNotNull("Expected path is null, check your tests...", expectedPath); + assertNotNull(actualPath); + PathIterator expectedIterator = expectedPath.getPathIterator(null); PathIterator actualIterator = actualPath.getPathIterator(null); @@ -261,4 +267,37 @@ public class PathsTest { assertTrue("More points than expected", actualIterator.isDone()); } + + @Test + public void testWriteJPEG() throws IOException { + Path2D originalPath = readExpectedPath("/ser/multiple-clips.ser"); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_3BYTE_BGR); + try (ImageOutputStream stream = ImageIO.createImageOutputStream(bytes)) { + boolean written = Paths.writeClipped(image, originalPath, "JPEG", stream); + assertTrue(written); + } + assertTrue(bytes.size() > 1024); // Actual size may be plugin specific... + + Path2D actualPath = Paths.readPath(new ByteArrayImageInputStream(bytes.toByteArray())); + assertPathEquals(originalPath, actualPath); + } + + @Test + public void testWriteTIFF() throws IOException { + Path2D originalPath = readExpectedPath("/ser/grape-path.ser"); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB); + try (ImageOutputStream stream = ImageIO.createImageOutputStream(bytes)) { + boolean written = Paths.writeClipped(image, originalPath, "TIFF", stream); + assumeTrue(written); // TIFF support is optional + } + + assertTrue(bytes.size() > 1024); // Actual size may be plugin specific... + + Path2D actualPath = Paths.readPath(new ByteArrayImageInputStream(bytes.toByteArray())); + assertPathEquals(originalPath, actualPath); + } } From c48af5acc7bb302325d025109902844313772fed Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 22 Jan 2020 20:35:41 +0100 Subject: [PATCH 14/17] #490: Minor API clean-up and documentation. --- .../imageio/path/AdobePathReader.java | 2 +- .../imageio/path/AdobePathWriter.java | 31 +++++++++++++------ .../com/twelvemonkeys/imageio/path/Paths.java | 9 ++++-- 3 files changed, 30 insertions(+), 12 deletions(-) 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 index fefc07c5..88e24d6e 100755 --- 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 @@ -44,7 +44,7 @@ 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. + * Reads a {@code Shape} object from an Adobe Photoshop Path resource. * * @see Adobe Photoshop Path resource format * @author Jason Palmer, itemMaster LLC 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 index f3f01baf..b7916e31 100755 --- 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 @@ -49,7 +49,10 @@ import static com.twelvemonkeys.lang.Validate.isTrue; import static com.twelvemonkeys.lang.Validate.notNull; /** - * AdobePathWriter + * Writes a {@code Shape} object to an Adobe Photoshop Path or Path resource. + * + * @see Adobe Photoshop Path resource format + * @author Harald Kuhr */ public final class AdobePathWriter { @@ -181,14 +184,15 @@ public final class AdobePathWriter { } /** - * Writes the path as a complete Photoshop clipping path resource to the given stream. + * Writes the path as a complete Adobe Photoshop clipping path resource to the given stream. * * @param output the stream to write to. + * @param resourceId the resource id, typically {@link PSD#RES_CLIPPING_PATH} (0x07D0). * @throws IOException if an I/O exception happens during writing. */ - void writePathResource(final DataOutput output) throws IOException { + public void writePathResource(final DataOutput output, int resourceId) throws IOException { output.writeInt(PSD.RESOURCE_TYPE); - output.writeShort(PSD.RES_CLIPPING_PATH); + output.writeShort(resourceId); output.writeShort(0); // Path name (Pascal string) empty + pad output.writeInt(segments.size() * 26); // Resource size @@ -196,7 +200,7 @@ public final class AdobePathWriter { } /** - * Writes the path as a set of Adobe path segments to the given stream. + * Writes the path as a set of Adobe Photoshop path segments to the given stream. * * @param output the stream to write to. * @throws IOException if an I/O exception happens during writing. @@ -235,13 +239,17 @@ public final class AdobePathWriter { } } - // TODO: Do we need to care about endianness for TIFF files? - // TODO: Better name? - byte[] writePathResource() { + /** + * Transforms the path to a byte array, containing a complete Adobe Photoshop path resource. + * + * @param resourceId the resource id, typically {@link PSD#RES_CLIPPING_PATH} (0x07D0). + * @return a new byte array, containing the clipping path resource. + */ + public byte[] writePathResource(int resourceId) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try (DataOutputStream stream = new DataOutputStream(bytes)) { - writePathResource(stream); + writePathResource(stream, resourceId); } catch (IOException e) { throw new AssertionError("ByteArrayOutputStream threw IOException", e); @@ -250,6 +258,11 @@ public final class AdobePathWriter { return bytes.toByteArray(); } + /** + * Transforms the path to a byte array, containing a set of Adobe Photoshop path segments. + * + * @return a new byte array, containing the path segments. + */ public byte[] writePath() { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 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 index ce95a4f8..6e908def 100755 --- 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 @@ -260,7 +260,7 @@ public final class Paths { } /** - * Writes the image along with a clipping path resource, in the given format to the supplied output. + * Writes the image along with a clipping path resource, in the given format, to the supplied output. * The image is written to the * {@code ImageOutputStream} starting at the current stream * pointer, overwriting existing stream data from that point @@ -270,6 +270,11 @@ public final class Paths { * not close the output stream. * It is the responsibility of the caller to close the stream, if desired. *

+ *

+ * Implementation note: Only JPEG (using the "javax_imageio_jpeg_image_1.0" metadata format) and + * TIFF (using the "javax_imageio_tiff_image_1.0" or "com_sun_media_imageio_plugins_tiff_image_1.0" metadata formats) + * formats are currently supported. + *

* * @param image the image to be written, may not be {@code null}. * @param clipPath the clip path, may not be {@code null}. @@ -303,7 +308,7 @@ public final class Paths { IIOMetadata metadata = writer.getDefaultImageMetadata(type, param); List metadataFormats = asList(metadata.getMetadataFormatNames()); - byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(); + byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(PSD.RES_CLIPPING_PATH); if (metadataFormats.contains("javax_imageio_tiff_image_1.0") || metadataFormats.contains("com_sun_media_imageio_plugins_tiff_image_1.0")) { param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); From 5f9ea2e7c2af09d8f1d7651de0007009e1b07d93 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 22 Jan 2020 20:49:18 +0100 Subject: [PATCH 15/17] #490: Doc. --- .../src/main/java/com/twelvemonkeys/imageio/path/Paths.java | 1 + 1 file changed, 1 insertion(+) 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 index 6e908def..a906bf77 100755 --- 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 @@ -73,6 +73,7 @@ import static java.util.Collections.singletonMap; *
  • 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}
  • + *
  • Write an image with embedded path {@link #writeClipped}
  • * * * @see Adobe Photoshop Path resource format From c721291a782e7a50bf92e7a853cd9bd01be0e4d4 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 22 Jan 2020 20:51:39 +0100 Subject: [PATCH 16/17] #490: Last minute API changes... --- .../com/twelvemonkeys/imageio/path/AdobePathWriter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index b7916e31..40abc4b2 100755 --- 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 @@ -186,11 +186,11 @@ public final class AdobePathWriter { /** * Writes the path as a complete Adobe Photoshop clipping path resource to the given stream. * - * @param output the stream to write to. * @param resourceId the resource id, typically {@link PSD#RES_CLIPPING_PATH} (0x07D0). + * @param output the stream to write to. * @throws IOException if an I/O exception happens during writing. */ - public void writePathResource(final DataOutput output, int resourceId) throws IOException { + public void writePathResource(int resourceId, final DataOutput output) throws IOException { output.writeInt(PSD.RESOURCE_TYPE); output.writeShort(resourceId); output.writeShort(0); // Path name (Pascal string) empty + pad @@ -249,7 +249,7 @@ public final class AdobePathWriter { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try (DataOutputStream stream = new DataOutputStream(bytes)) { - writePathResource(stream, resourceId); + writePathResource(resourceId, stream); } catch (IOException e) { throw new AssertionError("ByteArrayOutputStream threw IOException", e); From 3d4bc0e69dc2ecce1bc50371b74ff4a38f8ed309 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Wed, 22 Jan 2020 21:03:02 +0100 Subject: [PATCH 17/17] #490: License. --- .../imageio/path/AdobePathBuilder.java | 30 +++++++++++++++++++ .../com/twelvemonkeys/imageio/path/Paths.java | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) 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 index 0a43af92..065fd864 100755 --- 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,3 +1,33 @@ +/* + * Copyright (c) 2020, 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 java.awt.geom.Path2D; 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 index a906bf77..f8db298b 100755 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Harald Kuhr + * Copyright (c) 2014-2020, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without