Merge pull request #516 from haraldk/adobe-path-write

Adobe Path write support
This commit is contained in:
Harald Kuhr 2020-01-22 21:38:43 +01:00 committed by GitHub
commit f7ce772f84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1530 additions and 397 deletions

View File

@ -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 <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
* @author <a href="mailto:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<List<AdobePathSegment>> subPaths = new ArrayList<List<AdobePathSegment>>();
List<AdobePathSegment> currentPath = null;
int currentPathLength = 0;
AdobePathSegment segment;
while ((segment = nextSegment()) != null) {
if (DEBUG) {
System.out.println(segment);
}
if (segment.selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD || segment.selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD) {
if (currentPath != null) {
if (currentPathLength != currentPath.size()) {
throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
}
subPaths.add(currentPath);
}
currentPath = new ArrayList<AdobePathSegment>(segment.length);
currentPathLength = segment.length;
}
else if (segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED
|| segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED
|| segment.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED
|| segment.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED) {
if (currentPath == null) {
throw new IIOException("Bad path, missing subpath length record");
}
if (currentPath.size() >= currentPathLength) {
throw new IIOException(String.format("Bad path, expected %d segments, found%d", currentPathLength, currentPath.size()));
}
currentPath.add(segment);
}
}
// now add the last one
if (currentPath != null) {
if (currentPathLength != currentPath.size()) {
throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
}
subPaths.add(currentPath);
}
// now we have collected the PathPoints now create a Shape.
return pathToShape(subPaths);
}
/**
* The Correct Order... P1, P2, P3, P4, P5, P6 (Closed) moveTo(P1)
* curveTo(P1.cpl, P2.cpp, P2.ap); curveTo(P2.cpl, P3.cppy, P3.ap);
* curveTo(P3.cpl, P4.cpp, P4.ap); curveTo(P4.cpl, P5.cpp, P5.ap);
* curveTo(P5.cply, P6.cpp, P6.ap); curveTo(P6.cpl, P1.cpp, P1.ap);
* closePath()
*/
private Path2D pathToShape(final List<List<AdobePathSegment>> paths) {
GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD, paths.size());
GeneralPath subpath = null;
for (List<AdobePathSegment> points : paths) {
int length = points.size();
for (int i = 0; i < points.size(); i++) {
AdobePathSegment current = points.get(i);
int step = i == 0 ? 0 : i == length - 1 ? 2 : 1;
switch (step) {
// begin
case 0: {
subpath = new GeneralPath(Path2D.WIND_EVEN_ODD, length);
subpath.moveTo(current.apx, current.apy);
if (length > 1) {
AdobePathSegment next = points.get((i + 1));
subpath.curveTo(current.cplx, current.cply, next.cppx, next.cppy, next.apx, next.apy);
}
else {
subpath.lineTo(current.apx, current.apy);
}
break;
}
// middle
case 1: {
AdobePathSegment next = points.get((i + 1)); // we are always guaranteed one more.
subpath.curveTo(current.cplx, current.cply, next.cppx, next.cppy, next.apx, next.apy);
break;
}
// end
case 2: {
AdobePathSegment first = points.get(0);
if (first.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED || first.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED) {
subpath.curveTo(current.cplx, current.cply, first.cppx, first.cppy, first.apx, first.apy);
subpath.closePath();
path.append(subpath, false);
}
else {
subpath.lineTo(current.apx, current.apy);
path.append(subpath, true);
}
break;
}
}
}
}
return path;
}
private AdobePathSegment nextSegment() throws IOException {
// Each segment is 26 bytes
int selector;
try {
selector = data.readUnsignedShort();
}
catch (EOFException eof) {
// No more data, we're done
return null;
}
// Spec says Fill rule is ignored by Photoshop... Probably not.. ;-)
// TODO: Replace with switch + handle all types!
// TODO: ...or Move logic to AdobePathSegment?
if (selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD || selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD) {
int size = data.readUnsignedShort();
// data.position(data.position() + 22); // Skip remaining
data.skipBytes(22);
return new AdobePathSegment(selector, size);
}
return new AdobePathSegment(
selector,
readFixedPoint(data.readInt()),
readFixedPoint(data.readInt()),
readFixedPoint(data.readInt()),
readFixedPoint(data.readInt()),
readFixedPoint(data.readInt()),
readFixedPoint(data.readInt())
);
}
private static double readFixedPoint(final int fixed) {
return ((double) fixed / 0x1000000);
return delegate.readPath();
}
}

View File

@ -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 <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
* @author <a href="mailto:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public final class 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<List<AdobePathSegment>> subPaths = new ArrayList<>();
List<AdobePathSegment> currentPath = null;
int currentPathLength = 0;
AdobePathSegment segment;
while ((segment = nextSegment()) != null) {
if (DEBUG) {
System.out.println(segment);
}
if (segment.selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD || segment.selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD) {
if (currentPath != null) {
if (currentPathLength != currentPath.size()) {
throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
}
subPaths.add(currentPath);
}
currentPath = new ArrayList<>(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<List<AdobePathSegment>> paths) {
Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD, paths.size());
Path2D subpath = null;
for (List<AdobePathSegment> points : paths) {
int length = points.size();
for (int i = 0; i < points.size(); i++) {
AdobePathSegment current = points.get(i);
int step = i == 0 ? 0 : i == length - 1 ? 2 : 1;
switch (step) {
// Begin
case 0: {
subpath = new 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())
);
}
}
}

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
final class AdobePathSegment {
public final static int CLOSED_SUBPATH_LENGTH_RECORD = 0;
public final static int CLOSED_SUBPATH_BEZIER_LINKED = 1;
public final static int CLOSED_SUBPATH_BEZIER_UNLINKED = 2;
public final static int OPEN_SUBPATH_LENGTH_RECORD = 3;
public final static int OPEN_SUBPATH_BEZIER_LINKED = 4;
public final static int OPEN_SUBPATH_BEZIER_UNLINKED = 5;
public final static int PATH_FILL_RULE_RECORD = 6;
public final static int CLIPBOARD_RECORD = 7;
public final static int INITIAL_FILL_RULE_RECORD = 8;
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]);
}
}

View File

@ -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 <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
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<AdobePathSegment> segments;
/**
* Creates an AdobePathWriter for the given path.
* <p>
* 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.
* </p>
*
* @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<List<AdobePathSegment>...
private static List<AdobePathSegment> 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<AdobePathSegment> subpath = new ArrayList<>();
List<AdobePathSegment> 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<AdobePathSegment> subpath, List<AdobePathSegment> 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();
}
}

View File

@ -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;
* <li>Extract a path from an image input stream, {@link #readPath}</li>
* <li>Apply a given path to a given {@code BufferedImage} {@link #applyClippingPath}</li>
* <li>Read an image with path applied {@link #readClipped}</li>
* <li>Write an image with embedded path {@link #writeClipped}</li>
* </ul>
*
* @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
* @see com.twelvemonkeys.imageio.path.AdobePathBuilder
* @see AdobePathReader
* @author <a href="mailto:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
@ -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<Integer, java.util.List<String>> segmentIdentifiers = new LinkedHashMap<>();
segmentIdentifiers.put(JPEG.APP13, singletonList("Photoshop 3.0"));
Map<Integer, List<String>> segmentIdentifiers = singletonMap(JPEG.APP13, singletonList("Photoshop 3.0"));
List<JPEGSegment> 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.
* <p>
* Note: As {@link ImageIO#write(RenderedImage, String, ImageOutputStream)}, this method does
* <em>not</em> close the output stream.
* It is the responsibility of the caller to close the stream, if desired.
* </p>
* <p>
* 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.
* </p>
*
* @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<ImageWriter> writers = ImageIO.getImageWriters(type, formatName);
if (writers.hasNext()) {
ImageWriter writer = writers.next();
ImageWriteParam param = writer.getDefaultWriteParam();
IIOMetadata metadata = writer.getDefaultImageMetadata(type, param);
List<String> 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);
}
}
}

View File

@ -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)

View File

@ -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"));
}
}

View File

@ -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

View File

@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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<Integer, Entry> 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<Integer, Entry> 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());
}

View File

@ -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)) {