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..065fd864 --- 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,5 +1,5 @@ /* - * Copyright (c) 2014, Harald Kuhr + * Copyright (c) 2020, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -30,215 +30,37 @@ 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 DataInput data; + private final AdobePathReader delegate; /** - * 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} + * @see AdobePathReader#AdobePathReader(DataInput) */ public AdobePathBuilder(final DataInput data) { - notNull(data, "data"); - this.data = data; + this.delegate = new AdobePathReader(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. + * @see AdobePathReader#AdobePathReader(byte[]) */ 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); } /** - * 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. + * @see AdobePathReader#readPath() */ 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.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 new file mode 100755 index 00000000..88e24d6e --- /dev/null +++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathReader.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2014-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 javax.imageio.IIOException; +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; + +/** + * Reads 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 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. + * @throws java.lang.IllegalArgumentException if {@code data} is {@code null} + */ + public AdobePathReader(final DataInput data) { + notNull(data, "data"); + this.data = data; + } + + /** + * 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. + * @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 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 readPath() 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.lengthOrRule); + currentPathLength = segment.lengthOrRule; + } + 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); + } + + // We have collected the Path points, 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) { + Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD, paths.size()); + Path2D 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 Path2D.Float(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; + } + default: + throw new AssertionError(); + } + } + } + + 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, we'll read it anyway + case AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD: + case AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD: + int lengthOrRule = data.readUnsignedShort(); + data.skipBytes(22); + return new AdobePathSegment(selector, lengthOrRule); + default: + return new AdobePathSegment( + selector, + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()), + AdobePathSegment.fromFixedPoint(data.readInt()) + ); + } + } + +} 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..739e219d --- 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 @@ -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", @@ -63,12 +63,18 @@ final class AdobePathSegment { }; final int selector; - final int length; + final int lengthOrRule; + // 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,11 +85,14 @@ 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(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) { @@ -91,27 +100,29 @@ final class AdobePathSegment { switch (selector) { case CLOSED_SUBPATH_LENGTH_RECORD: case OPEN_SUBPATH_LENGTH_RECORD: - Validate.isTrue(length >= 0, length, "Bad size: %d"); + isTrue(lengthOrRule >= 0, lengthOrRule, "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: - 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("Bad selector: " + selector); + throw new IllegalArgumentException("Unknown selector: " + selector); } this.selector = selector; - this.length = length; + this.lengthOrRule = lengthOrRule; this.cppy = cppy; this.cppx = cppx; this.apy = apy; @@ -120,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) { @@ -139,7 +158,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; } @@ -148,7 +167,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); @@ -170,13 +189,13 @@ 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, totalPoints=%d)", SELECTOR_NAMES[selector], length); + return String.format("Len(selector=%s, length=%d)", SELECTOR_NAMES[selector], lengthOrRule); 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..40abc4b2 --- /dev/null +++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java @@ -0,0 +1,278 @@ +/* + * 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; + +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.AdobePathReader.DEBUG; +import static com.twelvemonkeys.imageio.path.AdobePathSegment.*; +import static com.twelvemonkeys.lang.Validate.isTrue; +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * 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 { + + // 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; + + /** + * 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 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] or + * the path is not closed. + */ + public AdobePathWriter(final Shape path) { + notNull(path, "path"); + 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) { + // 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); + + List subpath = new ArrayList<>(); + List segments = new ArrayList<>(); + 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); + + if (DEBUG) { + System.out.println("segmentType: " + segmentType); + System.out.println("coords: " + Arrays.toString(coords)); + } + + // 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: + // 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(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(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(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; + + case PathIterator.SEG_CLOSE: + 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); + 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); + } + + close(initial, prev, subpath, segments); + subpath.clear(); + + break; + } + + pathIterator.next(); + } + + // 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) || + (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 Adobe Photoshop clipping path resource to the given stream. + * + * @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(int resourceId, final DataOutput output) throws IOException { + output.writeInt(PSD.RESOURCE_TYPE); + output.writeShort(resourceId); + output.writeShort(0); // Path name (Pascal string) empty + pad + output.writeInt(segments.size() * 26); // Resource size + + writePath(output); + } + + /** + * 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. + */ + public void writePath(final DataOutput output) throws IOException { + if (DEBUG) { + System.out.println("segments: " + segments.size()); + System.out.println(segments); + } + + for (AdobePathSegment segment : segments) { + 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.lengthOrRule); // 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; + } + } + } + + /** + * 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(resourceId, stream); + } + catch (IOException e) { + throw new AssertionError("ByteArrayOutputStream threw IOException", e); + } + + 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(); + + try (DataOutputStream stream = new DataOutputStream(bytes)) { + writePath(stream); + } + catch (IOException 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 old mode 100644 new mode 100755 index 8d30112a..f8db298b --- 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 @@ -43,22 +43,29 @@ 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; 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.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.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; /** * Support for various Adobe Photoshop Path related operations: @@ -66,10 +73,11 @@ import static java.util.Collections.singletonList; *
  • 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 - * @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 +98,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 +107,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,17 +123,15 @@ 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 - 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()) { - 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 +143,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 +162,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); + Entry pathResource = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH); - if (resourceBlock != null) { - return new AdobePathBuilder((byte[]) resourceBlock.getValue()).path(); + if (pathResource != null) { + return new AdobePathReader((byte[]) pathResource.getValue()).readPath(); } return null; @@ -254,9 +260,149 @@ public final class Paths { return applyClippingPath(clip, image); } + /** + * 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. + *

    + *

    + * 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}. + * @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!"); + } + if (formatName == null) { + throw new IllegalArgumentException("formatName == null!"); + } + if (output == null) { + throw new IllegalArgumentException("output == null!"); + } + + 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); + List metadataFormats = asList(metadata.getMetadataFormatNames()); + + 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); + 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"); + + 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); + + 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; + } + + 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(","); // NOTE: The javax_imageio_tiff_image_1.0 format does not allow whitespace here... + } + } + // 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 + destination = readClipped(ImageIO.createImageInputStream(new File(args[0]))); + } + 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(); @@ -270,5 +416,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 c42aee6a..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,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, 0).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 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..5179ed1e --- /dev/null +++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java @@ -0,0 +1,416 @@ +/* + * 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(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); + path.moveTo(.5, .5); + path.lineTo(1, .5); + path.curveTo(1, 1, 1, 1, .5, 1); + path.closePath(); + + 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); + } + + @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); + + 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); + + } + + @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(); + + 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); + } + + @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(); + + 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); + + // 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); + } + + 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) { + 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); + + // 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); + } +} 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..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. @@ -125,12 +128,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 +150,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 +164,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 +213,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 +234,70 @@ 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) { + assertNotNull("Expected path is null, check your tests...", expectedPath); + assertNotNull(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()); + } + + @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); } } 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; 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 594db004..e7199b8b 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)) {