mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-05 04:25:29 -04:00
Merge pull request #516 from haraldk/adobe-path-write
Adobe Path write support
This commit is contained in:
commit
f7ce772f84
198
imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathBuilder.java
Normal file → Executable file
198
imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathBuilder.java
Normal file → Executable file
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2014, Harald Kuhr
|
* Copyright (c) 2020, Harald Kuhr
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
@ -30,215 +30,37 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.path;
|
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.awt.geom.Path2D;
|
||||||
import java.io.DataInput;
|
import java.io.DataInput;
|
||||||
import java.io.EOFException;
|
|
||||||
import java.io.IOException;
|
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>
|
* @deprecated Use {@link AdobePathReader} instead. This class will be removed in a future release.
|
||||||
* @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 AdobePathBuilder {
|
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
|
* @see AdobePathReader#AdobePathReader(DataInput)
|
||||||
* {@code ImageInputStream}.
|
|
||||||
* The data length is assumed to be a multiple of 26.
|
|
||||||
*
|
|
||||||
* @param data the input to read data from.
|
|
||||||
* @throws java.lang.IllegalArgumentException if {@code data} is {@code null}
|
|
||||||
*/
|
*/
|
||||||
public AdobePathBuilder(final DataInput data) {
|
public AdobePathBuilder(final DataInput data) {
|
||||||
notNull(data, "data");
|
this.delegate = new AdobePathReader(data);
|
||||||
this.data = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a path builder that will read its data from a {@code byte} array.
|
* @see AdobePathReader#AdobePathReader(byte[])
|
||||||
* The array length must be a multiple of 26, and greater than 0.
|
|
||||||
*
|
|
||||||
* @param data the array to read data from.
|
|
||||||
* @throws java.lang.IllegalArgumentException if {@code data} is {@code null}, or not a multiple of 26.
|
|
||||||
*/
|
*/
|
||||||
public AdobePathBuilder(final byte[] data) {
|
public AdobePathBuilder(final byte[] data) {
|
||||||
this(new ByteArrayImageInputStream(
|
this.delegate = new AdobePathReader(data);
|
||||||
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.
|
* @see AdobePathReader#readPath()
|
||||||
*
|
|
||||||
* @return the path
|
|
||||||
* @throws javax.imageio.IIOException if the input contains a bad path data.
|
|
||||||
* @throws IOException if a general I/O exception occurs during reading.
|
|
||||||
*/
|
*/
|
||||||
public Path2D path() throws IOException {
|
public Path2D path() throws IOException {
|
||||||
List<List<AdobePathSegment>> subPaths = new ArrayList<List<AdobePathSegment>>();
|
return delegate.readPath();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
73
imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathSegment.java
Normal file → Executable file
73
imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathSegment.java
Normal file → Executable file
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2014, Harald Kuhr
|
* Copyright (c) 2014-2020, Harald Kuhr
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* Redistribution and use in source and binary forms, with or without
|
||||||
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.path;
|
package com.twelvemonkeys.imageio.path;
|
||||||
|
|
||||||
import com.twelvemonkeys.lang.Validate;
|
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adobe path segment.
|
* Adobe path segment.
|
||||||
@ -40,17 +40,17 @@ import com.twelvemonkeys.lang.Validate;
|
|||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
*/
|
*/
|
||||||
final class AdobePathSegment {
|
final class AdobePathSegment {
|
||||||
public final static int CLOSED_SUBPATH_LENGTH_RECORD = 0;
|
static final int CLOSED_SUBPATH_LENGTH_RECORD = 0;
|
||||||
public final static int CLOSED_SUBPATH_BEZIER_LINKED = 1;
|
static final int CLOSED_SUBPATH_BEZIER_LINKED = 1;
|
||||||
public final static int CLOSED_SUBPATH_BEZIER_UNLINKED = 2;
|
static final int CLOSED_SUBPATH_BEZIER_UNLINKED = 2;
|
||||||
public final static int OPEN_SUBPATH_LENGTH_RECORD = 3;
|
static final int OPEN_SUBPATH_LENGTH_RECORD = 3;
|
||||||
public final static int OPEN_SUBPATH_BEZIER_LINKED = 4;
|
static final int OPEN_SUBPATH_BEZIER_LINKED = 4;
|
||||||
public final static int OPEN_SUBPATH_BEZIER_UNLINKED = 5;
|
static final int OPEN_SUBPATH_BEZIER_UNLINKED = 5;
|
||||||
public final static int PATH_FILL_RULE_RECORD = 6;
|
static final int PATH_FILL_RULE_RECORD = 6;
|
||||||
public final static int CLIPBOARD_RECORD = 7;
|
static final int CLIPBOARD_RECORD = 7;
|
||||||
public final static int INITIAL_FILL_RULE_RECORD = 8;
|
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 length record",
|
||||||
"Closed subpath Bezier knot, linked",
|
"Closed subpath Bezier knot, linked",
|
||||||
"Closed subpath Bezier knot, unlinked",
|
"Closed subpath Bezier knot, unlinked",
|
||||||
@ -63,12 +63,18 @@ final class AdobePathSegment {
|
|||||||
};
|
};
|
||||||
|
|
||||||
final int selector;
|
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 cppy;
|
||||||
final double cppx;
|
final double cppx;
|
||||||
|
|
||||||
|
// Anchor point
|
||||||
final double apy;
|
final double apy;
|
||||||
final double apx;
|
final double apx;
|
||||||
|
|
||||||
|
// Control point leaving knot
|
||||||
final double cply;
|
final double cply;
|
||||||
final double cplx;
|
final double cplx;
|
||||||
|
|
||||||
@ -79,11 +85,14 @@ final class AdobePathSegment {
|
|||||||
this(selector, -1, cppy, cppx, apy, apx, cply, cplx);
|
this(selector, -1, cppy, cppx, apy, apx, cply, cplx);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdobePathSegment(final int selector, final int length) {
|
AdobePathSegment(final int selector, final int lengthOrRule) {
|
||||||
this(selector, length, -1, -1, -1, -1, -1, -1);
|
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 cppy, final double cppx,
|
||||||
final double apy, final double apx,
|
final double apy, final double apx,
|
||||||
final double cply, final double cplx) {
|
final double cply, final double cplx) {
|
||||||
@ -91,27 +100,29 @@ final class AdobePathSegment {
|
|||||||
switch (selector) {
|
switch (selector) {
|
||||||
case CLOSED_SUBPATH_LENGTH_RECORD:
|
case CLOSED_SUBPATH_LENGTH_RECORD:
|
||||||
case OPEN_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;
|
break;
|
||||||
case CLOSED_SUBPATH_BEZIER_LINKED:
|
case CLOSED_SUBPATH_BEZIER_LINKED:
|
||||||
case CLOSED_SUBPATH_BEZIER_UNLINKED:
|
case CLOSED_SUBPATH_BEZIER_UNLINKED:
|
||||||
case OPEN_SUBPATH_BEZIER_LINKED:
|
case OPEN_SUBPATH_BEZIER_LINKED:
|
||||||
case OPEN_SUBPATH_BEZIER_UNLINKED:
|
case OPEN_SUBPATH_BEZIER_UNLINKED:
|
||||||
Validate.isTrue(
|
isTrue(
|
||||||
cppx >= 0 && cppx <= 1 && cppy >= 0 && cppy <= 1,
|
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;
|
break;
|
||||||
case PATH_FILL_RULE_RECORD:
|
case PATH_FILL_RULE_RECORD:
|
||||||
case CLIPBOARD_RECORD:
|
|
||||||
case INITIAL_FILL_RULE_RECORD:
|
case INITIAL_FILL_RULE_RECORD:
|
||||||
|
isTrue(lengthOrRule == 0 || lengthOrRule == 1, lengthOrRule, "Expected rule (1 or 0): %d");
|
||||||
|
break;
|
||||||
|
case CLIPBOARD_RECORD:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Bad selector: " + selector);
|
throw new IllegalArgumentException("Unknown selector: " + selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.length = length;
|
this.lengthOrRule = lengthOrRule;
|
||||||
this.cppy = cppy;
|
this.cppy = cppy;
|
||||||
this.cppx = cppx;
|
this.cppx = cppx;
|
||||||
this.apy = apy;
|
this.apy = apy;
|
||||||
@ -120,6 +131,14 @@ final class AdobePathSegment {
|
|||||||
this.cplx = cplx;
|
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
|
@Override
|
||||||
public boolean equals(final Object other) {
|
public boolean equals(final Object other) {
|
||||||
if (this == other) {
|
if (this == other) {
|
||||||
@ -139,7 +158,7 @@ final class AdobePathSegment {
|
|||||||
&& Double.compare(that.cppx, cppx) == 0
|
&& Double.compare(that.cppx, cppx) == 0
|
||||||
&& Double.compare(that.cppy, cppy) == 0
|
&& Double.compare(that.cppy, cppy) == 0
|
||||||
&& selector == that.selector
|
&& selector == that.selector
|
||||||
&& length == that.length;
|
&& lengthOrRule == that.lengthOrRule;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +167,7 @@ final class AdobePathSegment {
|
|||||||
long tempBits;
|
long tempBits;
|
||||||
|
|
||||||
int result = selector;
|
int result = selector;
|
||||||
result = 31 * result + length;
|
result = 31 * result + lengthOrRule;
|
||||||
tempBits = Double.doubleToLongBits(cppy);
|
tempBits = Double.doubleToLongBits(cppy);
|
||||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||||
tempBits = Double.doubleToLongBits(cppx);
|
tempBits = Double.doubleToLongBits(cppx);
|
||||||
@ -170,13 +189,13 @@ final class AdobePathSegment {
|
|||||||
switch (selector) {
|
switch (selector) {
|
||||||
case INITIAL_FILL_RULE_RECORD:
|
case INITIAL_FILL_RULE_RECORD:
|
||||||
case PATH_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 CLOSED_SUBPATH_LENGTH_RECORD:
|
||||||
case OPEN_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:
|
default:
|
||||||
// fall-through
|
// 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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
183
imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
Normal file → Executable file
183
imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
Normal file → Executable file
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2014, Harald Kuhr
|
* Copyright (c) 2014-2020, Harald Kuhr
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without
|
* 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.ByteArrayImageInputStream;
|
||||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
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.ImageInputStream;
|
||||||
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
import java.awt.geom.Path2D;
|
import java.awt.geom.Path2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.RenderedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedHashMap;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
|
import static java.util.Collections.singletonMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Support for various Adobe Photoshop Path related operations:
|
* 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>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>Apply a given path to a given {@code BufferedImage} {@link #applyClippingPath}</li>
|
||||||
* <li>Read an image with path applied {@link #readClipped}</li>
|
* <li>Read an image with path applied {@link #readClipped}</li>
|
||||||
|
* <li>Write an image with embedded path {@link #writeClipped}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @see <a href="http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17587">Adobe Photoshop Path resource format</a>
|
* @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:jpalmer@itemmaster.com">Jason Palmer, itemMaster LLC</a>
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $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 javax.imageio.IIOException if the input contains a bad path data.
|
||||||
* @throws java.lang.IllegalArgumentException is {@code stream} is {@code null}.
|
* @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 {
|
public static Path2D readPath(final ImageInputStream stream) throws IOException {
|
||||||
notNull(stream, "stream");
|
notNull(stream, "stream");
|
||||||
@ -99,7 +107,7 @@ public final class Paths {
|
|||||||
|
|
||||||
if (magic == PSD.RESOURCE_TYPE) {
|
if (magic == PSD.RESOURCE_TYPE) {
|
||||||
// This is a PSD Image Resource Block, we can parse directly
|
// This is a PSD Image Resource Block, we can parse directly
|
||||||
return buildPathFromPhotoshopResources(stream);
|
return readPathFromPhotoshopResources(stream);
|
||||||
}
|
}
|
||||||
else if (magic == PSD.SIGNATURE_8BPS) {
|
else if (magic == PSD.SIGNATURE_8BPS) {
|
||||||
// PSD version
|
// PSD version
|
||||||
@ -115,17 +123,15 @@ public final class Paths {
|
|||||||
long imageResourcesLen = stream.readUnsignedInt();
|
long imageResourcesLen = stream.readUnsignedInt();
|
||||||
|
|
||||||
// Image resources
|
// Image resources
|
||||||
return buildPathFromPhotoshopResources(new SubImageInputStream(stream, imageResourcesLen));
|
return readPathFromPhotoshopResources(new SubImageInputStream(stream, imageResourcesLen));
|
||||||
}
|
}
|
||||||
else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) {
|
else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) {
|
||||||
// JPEG version
|
// JPEG version
|
||||||
Map<Integer, java.util.List<String>> segmentIdentifiers = new LinkedHashMap<>();
|
Map<Integer, List<String>> segmentIdentifiers = singletonMap(JPEG.APP13, singletonList("Photoshop 3.0"));
|
||||||
segmentIdentifiers.put(JPEG.APP13, singletonList("Photoshop 3.0"));
|
|
||||||
|
|
||||||
List<JPEGSegment> photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
|
List<JPEGSegment> photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
|
||||||
|
|
||||||
if (!photoshop.isEmpty()) {
|
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
|
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);
|
Entry photoshop = directory.getEntryById(TIFF.TAG_PHOTOSHOP);
|
||||||
|
|
||||||
if (photoshop != null) {
|
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);
|
Directory resourceBlocks = new PSDReader().read(stream);
|
||||||
|
|
||||||
if (AdobePathBuilder.DEBUG) {
|
if (AdobePathReader.DEBUG) {
|
||||||
System.out.println("resourceBlocks: " + resourceBlocks);
|
System.out.println("resourceBlocks: " + resourceBlocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry resourceBlock = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH);
|
Entry pathResource = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH);
|
||||||
|
|
||||||
if (resourceBlock != null) {
|
if (pathResource != null) {
|
||||||
return new AdobePathBuilder((byte[]) resourceBlock.getValue()).path();
|
return new AdobePathReader((byte[]) pathResource.getValue()).readPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -254,9 +260,149 @@ public final class Paths {
|
|||||||
return applyClippingPath(clip, image);
|
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
|
// Test code
|
||||||
public static void main(final String[] args) throws IOException, InterruptedException {
|
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");
|
File tempFile = File.createTempFile("clipped-", ".png");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
@ -270,5 +416,4 @@ public final class Paths {
|
|||||||
System.err.printf("%s not deleted\n", tempFile);
|
System.err.printf("%s not deleted\n", tempFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals;
|
|||||||
import static com.twelvemonkeys.imageio.path.PathsTest.readExpectedPath;
|
import static com.twelvemonkeys.imageio.path.PathsTest.readExpectedPath;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public class AdobePathBuilderTest {
|
public class AdobePathBuilderTest {
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
@ -63,7 +63,7 @@ public class AdobePathSegmentTest {
|
|||||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42);
|
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42);
|
||||||
|
|
||||||
assertEquals(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, segment.selector);
|
assertEquals(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, segment.selector);
|
||||||
assertEquals(42, segment.length);
|
assertEquals(42, segment.lengthOrRule);
|
||||||
assertEquals(-1, segment.cppx, 0);
|
assertEquals(-1, segment.cppx, 0);
|
||||||
assertEquals(-1, segment.cppy, 0);
|
assertEquals(-1, segment.cppy, 0);
|
||||||
assertEquals(-1, segment.apx, 0);
|
assertEquals(-1, segment.apx, 0);
|
||||||
@ -82,7 +82,7 @@ public class AdobePathSegmentTest {
|
|||||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 27);
|
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 27);
|
||||||
|
|
||||||
assertEquals(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, segment.selector);
|
assertEquals(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, segment.selector);
|
||||||
assertEquals(27, segment.length);
|
assertEquals(27, segment.lengthOrRule);
|
||||||
assertEquals(-1, segment.cppx, 0);
|
assertEquals(-1, segment.cppx, 0);
|
||||||
assertEquals(-1, segment.cppy, 0);
|
assertEquals(-1, segment.cppy, 0);
|
||||||
assertEquals(-1, segment.apx, 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);
|
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
|
||||||
|
|
||||||
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, segment.selector);
|
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, segment.selector);
|
||||||
assertEquals(-1, segment.length);
|
assertEquals(-1, segment.lengthOrRule);
|
||||||
assertEquals(.5, segment.cppx, 0);
|
assertEquals(.5, segment.cppx, 0);
|
||||||
assertEquals(.5, segment.cppy, 0);
|
assertEquals(.5, segment.cppy, 0);
|
||||||
assertEquals(0, segment.apx, 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);
|
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
|
||||||
|
|
||||||
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, segment.selector);
|
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, segment.selector);
|
||||||
assertEquals(-1, segment.length);
|
assertEquals(-1, segment.lengthOrRule);
|
||||||
assertEquals(.5, segment.cppx, 0);
|
assertEquals(.5, segment.cppx, 0);
|
||||||
assertEquals(.5, segment.cppy, 0);
|
assertEquals(.5, segment.cppy, 0);
|
||||||
assertEquals(0, segment.apx, 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);
|
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
|
||||||
|
|
||||||
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, segment.selector);
|
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, segment.selector);
|
||||||
assertEquals(-1, segment.length);
|
assertEquals(-1, segment.lengthOrRule);
|
||||||
assertEquals(.5, segment.cppx, 0);
|
assertEquals(.5, segment.cppx, 0);
|
||||||
assertEquals(.5, segment.cppy, 0);
|
assertEquals(.5, segment.cppy, 0);
|
||||||
assertEquals(0, segment.apx, 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);
|
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
|
||||||
|
|
||||||
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, segment.selector);
|
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, segment.selector);
|
||||||
assertEquals(-1, segment.length);
|
assertEquals(-1, segment.lengthOrRule);
|
||||||
assertEquals(.5, segment.cppx, 0);
|
assertEquals(.5, segment.cppx, 0);
|
||||||
assertEquals(.5, segment.cppy, 0);
|
assertEquals(.5, segment.cppy, 0);
|
||||||
assertEquals(0, segment.apx, 0);
|
assertEquals(0, segment.apx, 0);
|
||||||
@ -195,11 +195,11 @@ public class AdobePathSegmentTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testToStringRule() {
|
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.startsWith("Rule"));
|
||||||
assertTrue(string, string.contains("Initial"));
|
assertTrue(string, string.contains("Initial"));
|
||||||
assertTrue(string, string.contains("fill"));
|
assertTrue(string, string.contains("fill"));
|
||||||
assertTrue(string, string.contains("rule=2"));
|
assertTrue(string, string.contains("rule=0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -208,13 +208,13 @@ public class AdobePathSegmentTest {
|
|||||||
assertTrue(string, string.startsWith("Len"));
|
assertTrue(string, string.startsWith("Len"));
|
||||||
assertTrue(string, string.contains("Closed"));
|
assertTrue(string, string.contains("Closed"));
|
||||||
assertTrue(string, string.contains("subpath"));
|
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();
|
string = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42).toString();
|
||||||
assertTrue(string, string.startsWith("Len"));
|
assertTrue(string, string.startsWith("Len"));
|
||||||
assertTrue(string, string.contains("Open"));
|
assertTrue(string, string.contains("Open"));
|
||||||
assertTrue(string, string.contains("subpath"));
|
assertTrue(string, string.contains("subpath"));
|
||||||
assertTrue(string, string.contains("totalPoints=42"));
|
assertTrue(string, string.contains("length=42"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -38,15 +38,18 @@ import org.junit.Test;
|
|||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.spi.IIORegistry;
|
import javax.imageio.spi.IIORegistry;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.GeneralPath;
|
import java.awt.geom.GeneralPath;
|
||||||
import java.awt.geom.Path2D;
|
import java.awt.geom.Path2D;
|
||||||
import java.awt.geom.PathIterator;
|
import java.awt.geom.PathIterator;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectInputStream;
|
import java.io.ObjectInputStream;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PathsTest.
|
* PathsTest.
|
||||||
@ -125,12 +128,12 @@ public class PathsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void testApplyClippingPathNullPath() throws IOException {
|
public void testApplyClippingPathNullPath() {
|
||||||
Paths.applyClippingPath(null, new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY));
|
Paths.applyClippingPath(null, new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void testApplyClippingPathNullSource() throws IOException {
|
public void testApplyClippingPathNullSource() {
|
||||||
Paths.applyClippingPath(new GeneralPath(), null);
|
Paths.applyClippingPath(new GeneralPath(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +150,7 @@ public class PathsTest {
|
|||||||
assertEquals(source.getWidth(), image.getWidth());
|
assertEquals(source.getWidth(), image.getWidth());
|
||||||
assertEquals(source.getHeight(), image.getHeight());
|
assertEquals(source.getHeight(), image.getHeight());
|
||||||
// Transparent
|
// Transparent
|
||||||
assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
|
assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency());
|
||||||
|
|
||||||
// Corners (at least) should be transparent
|
// Corners (at least) should be transparent
|
||||||
assertEquals(0, image.getRGB(0, 0));
|
assertEquals(0, image.getRGB(0, 0));
|
||||||
@ -161,8 +164,9 @@ public class PathsTest {
|
|||||||
// TODO: Mor sophisticated test that tests all pixels outside path...
|
// TODO: Mor sophisticated test that tests all pixels outside path...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@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);
|
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(857, image.getWidth());
|
||||||
assertEquals(1800, image.getHeight());
|
assertEquals(1800, image.getHeight());
|
||||||
// Transparent
|
// Transparent
|
||||||
assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
|
assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency());
|
||||||
|
|
||||||
// Corners (at least) should be transparent
|
// Corners (at least) should be transparent
|
||||||
assertEquals(0, image.getRGB(0, 0));
|
assertEquals(0, image.getRGB(0, 0));
|
||||||
@ -230,34 +234,70 @@ public class PathsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Path2D readExpectedPath(final String resource) throws IOException {
|
static Path2D readExpectedPath(final String resource) throws IOException {
|
||||||
ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource));
|
try (ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource))) {
|
||||||
|
|
||||||
try {
|
|
||||||
return (Path2D) ois.readObject();
|
return (Path2D) ois.readObject();
|
||||||
}
|
}
|
||||||
catch (ClassNotFoundException e) {
|
catch (ClassNotFoundException e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
ois.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void assertPathEquals(final Path2D expectedPath, final Path2D actualPath) {
|
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 expectedIterator = expectedPath.getPathIterator(null);
|
||||||
PathIterator actualIterator = actualPath.getPathIterator(null);
|
PathIterator actualIterator = actualPath.getPathIterator(null);
|
||||||
|
|
||||||
float[] expectedCoords = new float[6];
|
float[] expectedCoords = new float[6];
|
||||||
float[] actualCoords = 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 expectedType = expectedIterator.currentSegment(expectedCoords);
|
||||||
int actualType = actualIterator.currentSegment(actualCoords);
|
int actualType = actualIterator.currentSegment(actualCoords);
|
||||||
|
|
||||||
assertEquals(expectedType, actualType);
|
assertEquals("Unexpected segment type", expectedType, actualType);
|
||||||
assertArrayEquals(expectedCoords, actualCoords, 0);
|
assertArrayEquals("Unexpected coordinates", expectedCoords, actualCoords, 0);
|
||||||
|
|
||||||
actualIterator.next();
|
actualIterator.next();
|
||||||
expectedIterator.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,6 @@ public final class PSDReader extends MetadataReader {
|
|||||||
|
|
||||||
PSDResource resource = new PSDResource(id, input);
|
PSDResource resource = new PSDResource(id, input);
|
||||||
entries.add(new PSDEntry(id, resource.name(), resource.data()));
|
entries.add(new PSDEntry(id, resource.name(), resource.data()));
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (EOFException e) {
|
catch (EOFException e) {
|
||||||
break;
|
break;
|
||||||
|
@ -144,6 +144,7 @@ public interface TIFF {
|
|||||||
int TAG_ROWS_PER_STRIP = 278;
|
int TAG_ROWS_PER_STRIP = 278;
|
||||||
int TAG_STRIP_BYTE_COUNTS = 279;
|
int TAG_STRIP_BYTE_COUNTS = 279;
|
||||||
int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange."
|
int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange."
|
||||||
|
int TAG_FREE_BYTE_COUNTS = 289;
|
||||||
// "Old-style" JPEG (still used as EXIF thumbnail)
|
// "Old-style" JPEG (still used as EXIF thumbnail)
|
||||||
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
|
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
|
||||||
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
|
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
|
||||||
|
@ -142,13 +142,17 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
|||||||
private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
|
private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
RenderedImage renderedImage = image.getRenderedImage();
|
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();
|
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 numBands = sampleModel.getNumBands();
|
||||||
int pixelSize = computePixelSize(sampleModel);
|
int pixelSize = computePixelSize(sampleModel);
|
||||||
|
|
||||||
@ -170,145 +174,29 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
|||||||
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
|
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<>();
|
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_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_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
|
// 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()));
|
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
|
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
|
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1)); // Updated later
|
||||||
|
|
||||||
// TODO: If tiled, write tile indexes etc
|
// TODO: If tiled, write tile indexes etc
|
||||||
// Depending on param.getTilingMode
|
// Depending on param.getTilingMode
|
||||||
long nextIFDPointerOffset = -1;
|
long nextIFDPointerOffset = -1;
|
||||||
|
|
||||||
|
int compression = ((Number) entries.get(TIFF.TAG_COMPRESSION).getValue()).intValue();
|
||||||
|
|
||||||
if (compression == TIFFBaseline.COMPRESSION_NONE) {
|
if (compression == TIFFBaseline.COMPRESSION_NONE) {
|
||||||
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
|
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
|
||||||
long streamPosition = imageOutput.getStreamPosition();
|
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);
|
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) {
|
if (ifd != null) {
|
||||||
for (Entry entry : ifd) {
|
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;
|
int compression;
|
||||||
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
|
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
|
||||||
&& ifd != null && ifd.getEntryById(TIFF.TAG_COMPRESSION) != null) {
|
&& ifd != null && ifd.getEntryById(TIFF.TAG_COMPRESSION) != null) {
|
||||||
@ -890,11 +824,81 @@ public final class TIFFImageWriter extends ImageWriterBase {
|
|||||||
else {
|
else {
|
||||||
compression = TIFFImageWriteParam.getCompressionType(param);
|
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));
|
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());
|
return new TIFFImageMetadata(entries.values());
|
||||||
}
|
}
|
||||||
|
@ -616,7 +616,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
|
|||||||
int maxH = Math.min(300, image.getHeight());
|
int maxH = Math.min(300, image.getHeight());
|
||||||
for (int y = 0; y < maxH; y++) {
|
for (int y = 0; y < maxH; y++) {
|
||||||
for (int x = 0; x < image.getWidth(); x++) {
|
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);
|
assumeNotNull(original);
|
||||||
|
|
||||||
// Write it back, using same compression (copied from metadata)
|
// Write it back, using deflate compression
|
||||||
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
|
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
|
||||||
|
|
||||||
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
|
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
|
||||||
@ -718,7 +718,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
|
|||||||
|
|
||||||
assumeNotNull(original);
|
assumeNotNull(original);
|
||||||
|
|
||||||
// Write it back, using same compression (copied from metadata)
|
// Write it back, no compression
|
||||||
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
|
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
|
||||||
|
|
||||||
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
|
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user