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