mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 20:15:28 -04:00
TMI-60: Support for clip paths in formats containing PSD resources
This commit is contained in:
parent
c2e9b585ff
commit
77e6600605
25
imageio/imageio-clippath/license.txt
Executable file
25
imageio/imageio-clippath/license.txt
Executable file
@ -0,0 +1,25 @@
|
||||
Copyright (c) 2014, Harald Kuhr
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name "TwelveMonkeys" 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 OWNER 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.
|
33
imageio/imageio-clippath/pom.xml
Executable file
33
imageio/imageio-clippath/pom.xml
Executable file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio</artifactId>
|
||||
<version>3.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>imageio-clippath</artifactId>
|
||||
<name>TwelveMonkeys :: ImageIO :: Photoshop Path Support</name>
|
||||
<description>
|
||||
Photoshop Clipping Path Support.
|
||||
</description>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||
<artifactId>imageio-metadata</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright (c) 2014, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" 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 OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.twelvemonkeys.imageio.path;
|
||||
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import java.awt.geom.GeneralPath;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.io.DataInput;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* Creates a {@code Shape} object from an Adobe Photoshop Path resource.
|
||||
*
|
||||
* @see <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 AdobePathBuilder {
|
||||
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.path.debug"));
|
||||
|
||||
private final DataInput data;
|
||||
|
||||
/**
|
||||
* Creates a path builder that will read its data from a {@code DataInput}, such as an
|
||||
* {@code ImageInputStream}.
|
||||
* The data length is assumed to be a multiple of 26.
|
||||
*
|
||||
* @param data the input to read data from.
|
||||
* @throws java.lang.IllegalArgumentException if {@code data} is {@code null}
|
||||
*/
|
||||
public AdobePathBuilder(final DataInput data) {
|
||||
notNull(data, "data");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a path builder that will read its data from a {@code byte} array.
|
||||
* The array length must be a multiple of 26, and greater than 0.
|
||||
*
|
||||
* @param data the array to read data from.
|
||||
* @throws java.lang.IllegalArgumentException if {@code data} is {@code null}, or not a multiple of 26.
|
||||
*/
|
||||
public AdobePathBuilder(final byte[] data) {
|
||||
this(new ByteArrayImageInputStream(
|
||||
notNull(data, "data"), 0,
|
||||
isTrue(data.length > 0 && data.length % 26 == 0, data.length, "data.length must be a multiple of 26: %d")
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the path.
|
||||
*
|
||||
* @return the path
|
||||
* @throws javax.imageio.IIOException if the input contains a bad path data.
|
||||
* @throws IOException if a general I/O exception occurs during reading.
|
||||
*/
|
||||
public Path2D path() throws IOException {
|
||||
List<List<AdobePathSegment>> subPaths = new ArrayList<List<AdobePathSegment>>();
|
||||
List<AdobePathSegment> currentPath = null;
|
||||
int currentPathLength = 0;
|
||||
|
||||
AdobePathSegment segment;
|
||||
while ((segment = nextSegment()) != null) {
|
||||
|
||||
if (DEBUG) {
|
||||
System.out.println(segment);
|
||||
}
|
||||
|
||||
if (segment.selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD || segment.selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD) {
|
||||
if (currentPath != null) {
|
||||
if (currentPathLength != currentPath.size()) {
|
||||
throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
|
||||
}
|
||||
subPaths.add(currentPath);
|
||||
}
|
||||
|
||||
currentPath = new ArrayList<AdobePathSegment>(segment.length);
|
||||
currentPathLength = segment.length;
|
||||
}
|
||||
else if (segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED
|
||||
|| segment.selector == AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED
|
||||
|| segment.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED
|
||||
|| segment.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED) {
|
||||
if (currentPath == null) {
|
||||
throw new IIOException("Bad path, missing subpath length record");
|
||||
}
|
||||
if (currentPath.size() >= currentPathLength) {
|
||||
throw new IIOException(String.format("Bad path, expected %d segments, found%d", currentPathLength, currentPath.size()));
|
||||
}
|
||||
|
||||
currentPath.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// now add the last one
|
||||
if (currentPath != null) {
|
||||
if (currentPathLength != currentPath.size()) {
|
||||
throw new IIOException(String.format("Bad path, expected %d segments, found only %d", currentPathLength, currentPath.size()));
|
||||
}
|
||||
|
||||
subPaths.add(currentPath);
|
||||
}
|
||||
|
||||
// now we have collected the PathPoints now create a Shape.
|
||||
return pathToShape(subPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Correct Order... P1, P2, P3, P4, P5, P6 (Closed) moveTo(P1)
|
||||
* curveTo(P1.cpl, P2.cpp, P2.ap); curveTo(P2.cpl, P3.cppy, P3.ap);
|
||||
* curveTo(P3.cpl, P4.cpp, P4.ap); curveTo(P4.cpl, P5.cpp, P5.ap);
|
||||
* curveTo(P5.cply, P6.cpp, P6.ap); curveTo(P6.cpl, P1.cpp, P1.ap);
|
||||
* closePath()
|
||||
*/
|
||||
private Path2D pathToShape(final List<List<AdobePathSegment>> paths) {
|
||||
GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD, paths.size());
|
||||
GeneralPath subpath = null;
|
||||
|
||||
for (List<AdobePathSegment> points : paths) {
|
||||
int length = points.size();
|
||||
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
AdobePathSegment current = points.get(i);
|
||||
|
||||
int step = i == 0 ? 0 : i == length - 1 ? 2 : 1;
|
||||
|
||||
switch (step) {
|
||||
// begin
|
||||
case 0: {
|
||||
subpath = new GeneralPath(Path2D.WIND_EVEN_ODD, length);
|
||||
subpath.moveTo(current.apx, current.apy);
|
||||
|
||||
if (length > 1) {
|
||||
AdobePathSegment next = points.get((i + 1));
|
||||
subpath.curveTo(current.cplx, current.cply, next.cppx, next.cppy, next.apx, next.apy);
|
||||
}
|
||||
else {
|
||||
subpath.lineTo(current.apx, current.apy);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// middle
|
||||
case 1: {
|
||||
AdobePathSegment next = points.get((i + 1)); // we are always guaranteed one more.
|
||||
subpath.curveTo(current.cplx, current.cply, next.cppx, next.cppy, next.apx, next.apy);
|
||||
|
||||
break;
|
||||
}
|
||||
// end
|
||||
case 2: {
|
||||
AdobePathSegment first = points.get(0);
|
||||
|
||||
if (first.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED || first.selector == AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED) {
|
||||
subpath.curveTo(current.cplx, current.cply, first.cppx, first.cppy, first.apx, first.apy);
|
||||
subpath.closePath();
|
||||
path.append(subpath, false);
|
||||
}
|
||||
else {
|
||||
subpath.lineTo(current.apx, current.apy);
|
||||
path.append(subpath, true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private AdobePathSegment nextSegment() throws IOException {
|
||||
// Each segment is 26 bytes
|
||||
int selector;
|
||||
try {
|
||||
selector = data.readUnsignedShort();
|
||||
}
|
||||
catch (EOFException eof) {
|
||||
// No more data, we're done
|
||||
return null;
|
||||
}
|
||||
|
||||
// Spec says Fill rule is ignored by Photoshop... Probably not.. ;-)
|
||||
// TODO: Replace with switch + handle all types!
|
||||
// TODO: ...or Move logic to AdobePathSegment?
|
||||
if (selector == AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD || selector == AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD) {
|
||||
int size = data.readUnsignedShort();
|
||||
// data.position(data.position() + 22); // Skip remaining
|
||||
data.skipBytes(22);
|
||||
return new AdobePathSegment(selector, size);
|
||||
}
|
||||
|
||||
return new AdobePathSegment(
|
||||
selector,
|
||||
readFixedPoint(data.readInt()),
|
||||
readFixedPoint(data.readInt()),
|
||||
readFixedPoint(data.readInt()),
|
||||
readFixedPoint(data.readInt()),
|
||||
readFixedPoint(data.readInt()),
|
||||
readFixedPoint(data.readInt())
|
||||
);
|
||||
}
|
||||
|
||||
private static double readFixedPoint(final int fixed) {
|
||||
return ((double) fixed / 0x1000000);
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (c) 2014, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" 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 OWNER 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.lang.Validate;
|
||||
|
||||
/**
|
||||
* Adobe path segment.
|
||||
*
|
||||
* @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>
|
||||
*/
|
||||
final class AdobePathSegment {
|
||||
public final static int CLOSED_SUBPATH_LENGTH_RECORD = 0;
|
||||
public final static int CLOSED_SUBPATH_BEZIER_LINKED = 1;
|
||||
public final static int CLOSED_SUBPATH_BEZIER_UNLINKED = 2;
|
||||
public final static int OPEN_SUBPATH_LENGTH_RECORD = 3;
|
||||
public final static int OPEN_SUBPATH_BEZIER_LINKED = 4;
|
||||
public final static int OPEN_SUBPATH_BEZIER_UNLINKED = 5;
|
||||
public final static int PATH_FILL_RULE_RECORD = 6;
|
||||
public final static int CLIPBOARD_RECORD = 7;
|
||||
public final static int INITIAL_FILL_RULE_RECORD = 8;
|
||||
|
||||
public final static String[] SELECTOR_NAMES = {
|
||||
"Closed subpath length record",
|
||||
"Closed subpath Bezier knot, linked",
|
||||
"Closed subpath Bezier knot, unlinked",
|
||||
"Open subpath length record",
|
||||
"Open subpath Bezier knot, linked",
|
||||
"Open subpath Bezier knot, unlinked",
|
||||
"Path fill rule record",
|
||||
"Clipboard record",
|
||||
"Initial fill rule record"
|
||||
};
|
||||
|
||||
final int selector;
|
||||
final int length;
|
||||
|
||||
final double cppy;
|
||||
final double cppx;
|
||||
final double apy;
|
||||
final double apx;
|
||||
final double cply;
|
||||
final double cplx;
|
||||
|
||||
AdobePathSegment(final int selector,
|
||||
final double cppy, final double cppx,
|
||||
final double apy, final double apx,
|
||||
final double cply, final double cplx) {
|
||||
this(selector, -1, cppy, cppx, apy, apx, cply, cplx);
|
||||
}
|
||||
|
||||
AdobePathSegment(final int selector, final int length) {
|
||||
this(selector, length, -1, -1, -1, -1, -1, -1);
|
||||
}
|
||||
|
||||
private AdobePathSegment(final int selector, final int length,
|
||||
final double cppy, final double cppx,
|
||||
final double apy, final double apx,
|
||||
final double cply, final double cplx) {
|
||||
// Validate selector, size and points
|
||||
switch (selector) {
|
||||
case CLOSED_SUBPATH_LENGTH_RECORD:
|
||||
case OPEN_SUBPATH_LENGTH_RECORD:
|
||||
Validate.isTrue(length >= 0, length, "Bad size: %d");
|
||||
break;
|
||||
case CLOSED_SUBPATH_BEZIER_LINKED:
|
||||
case CLOSED_SUBPATH_BEZIER_UNLINKED:
|
||||
case OPEN_SUBPATH_BEZIER_LINKED:
|
||||
case OPEN_SUBPATH_BEZIER_UNLINKED:
|
||||
Validate.isTrue(
|
||||
cppx >= 0 && cppx <= 1 && cppy >= 0 && cppy <= 1,
|
||||
String.format("Unexpected point: [%f, %f]", cppx ,cppy)
|
||||
);
|
||||
break;
|
||||
case PATH_FILL_RULE_RECORD:
|
||||
case CLIPBOARD_RECORD:
|
||||
case INITIAL_FILL_RULE_RECORD:
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Bad selector: " + selector);
|
||||
}
|
||||
|
||||
this.selector = selector;
|
||||
this.length = length;
|
||||
this.cppy = cppy;
|
||||
this.cppx = cppx;
|
||||
this.apy = apy;
|
||||
this.apx = apx;
|
||||
this.cply = cply;
|
||||
this.cplx = cplx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AdobePathSegment that = (AdobePathSegment) other;
|
||||
|
||||
return Double.compare(that.apx, apx) == 0
|
||||
&& Double.compare(that.apy, apy) == 0
|
||||
&& Double.compare(that.cplx, cplx) == 0
|
||||
&& Double.compare(that.cply, cply) == 0
|
||||
&& Double.compare(that.cppx, cppx) == 0
|
||||
&& Double.compare(that.cppy, cppy) == 0
|
||||
&& selector == that.selector
|
||||
&& length == that.length;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long tempBits;
|
||||
|
||||
int result = selector;
|
||||
result = 31 * result + length;
|
||||
tempBits = Double.doubleToLongBits(cppy);
|
||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||
tempBits = Double.doubleToLongBits(cppx);
|
||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||
tempBits = Double.doubleToLongBits(apy);
|
||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||
tempBits = Double.doubleToLongBits(apx);
|
||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||
tempBits = Double.doubleToLongBits(cply);
|
||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||
tempBits = Double.doubleToLongBits(cplx);
|
||||
result = 31 * result + (int) (tempBits ^ (tempBits >>> 32));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (selector) {
|
||||
case INITIAL_FILL_RULE_RECORD:
|
||||
case PATH_FILL_RULE_RECORD:
|
||||
return String.format("Rule(selector=%s, rule=%d)", SELECTOR_NAMES[selector], length);
|
||||
case CLOSED_SUBPATH_LENGTH_RECORD:
|
||||
case OPEN_SUBPATH_LENGTH_RECORD:
|
||||
return String.format("Len(selector=%s, totalPoints=%d)", SELECTOR_NAMES[selector], length);
|
||||
default:
|
||||
// fall-through
|
||||
}
|
||||
return String.format("Pt(preX=%.3f, preY=%.3f, knotX=%.3f, knotY=%.3f, postX=%.3f, postY=%.3f, selector=%s)", cppx, cppy, apx, apy, cplx, cply, SELECTOR_NAMES[selector]);
|
||||
}
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright (c) 2014, Harald Kuhr
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name "TwelveMonkeys" 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 OWNER 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.CompoundDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegment;
|
||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
|
||||
import com.twelvemonkeys.imageio.metadata.psd.PSD;
|
||||
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||
|
||||
/**
|
||||
* Support for various Adobe Photoshop Path related operations:
|
||||
* <ul>
|
||||
* <li>Extract a path from an image input stream, {@link #readPath}</li>
|
||||
* <li>Apply a given path to a given {@code BufferedImage} {@link #applyClippingPath}</li>
|
||||
* <li>Read an image with path applied {@link #readClipped}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: Paths.java,v 1.0 08/12/14 harald.kuhr Exp$
|
||||
*/
|
||||
public final class Paths {
|
||||
|
||||
private Paths() {}
|
||||
|
||||
/**
|
||||
* Reads the clipping path from the given input stream, if any.
|
||||
* Supports PSD, JPEG and TIFF as container formats for Photoshop resources,
|
||||
* or a "bare" PSD Image Resource Block.
|
||||
*
|
||||
* @param stream the input stream to read from, not {@code null}.
|
||||
* @return the path, or {@code null} if no path is found
|
||||
* @throws IOException if a general I/O exception occurs during reading.
|
||||
* @throws javax.imageio.IIOException if the input contains a bad path data.
|
||||
* @throws java.lang.IllegalArgumentException is {@code stream} is {@code null}.
|
||||
*
|
||||
* @see com.twelvemonkeys.imageio.path.AdobePathBuilder
|
||||
*/
|
||||
public static Path2D readPath(final ImageInputStream stream) throws IOException {
|
||||
notNull(stream, "stream");
|
||||
|
||||
int magic = readMagic(stream);
|
||||
|
||||
if (magic == PSD.RESOURCE_TYPE) {
|
||||
// This is a PSD Image Resource BLock, we can parse directly
|
||||
return buildPathFromPhotoshopResources(stream);
|
||||
}
|
||||
else if (magic == PSD.SIGNATURE_8BPS) {
|
||||
// PSD version
|
||||
// 4 byte magic, 2 byte version, 6 bytes reserved, 2 byte channels,
|
||||
// 4 byte height, 4 byte width, 2 byte bit depth, 2 byte mode
|
||||
stream.skipBytes(26);
|
||||
|
||||
// 4 byte color mode data length + n byte color mode data
|
||||
long colorModeLen = stream.readUnsignedInt();
|
||||
stream.skipBytes(colorModeLen);
|
||||
|
||||
// 4 byte image resources length
|
||||
long imageResourcesLen = stream.readUnsignedInt();
|
||||
|
||||
// Image resources
|
||||
return buildPathFromPhotoshopResources(new SubImageInputStream(stream, imageResourcesLen));
|
||||
}
|
||||
else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) {
|
||||
// JPEG version
|
||||
Map<Integer, java.util.List<String>> segmentIdentifiers = new LinkedHashMap<Integer, java.util.List<String>>();
|
||||
segmentIdentifiers.put(JPEG.APP13, Arrays.asList("Photoshop 3.0"));
|
||||
|
||||
List<JPEGSegment> photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
|
||||
|
||||
if (!photoshop.isEmpty()) {
|
||||
return buildPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data()));
|
||||
}
|
||||
}
|
||||
else if (magic >>> 16 == TIFF.BYTE_ORDER_MARK_BIG_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC
|
||||
|| magic >>> 16 == TIFF.BYTE_ORDER_MARK_LITTLE_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC << 8) {
|
||||
// TIFF version
|
||||
CompoundDirectory IFDs = (CompoundDirectory) new EXIFReader().read(stream);
|
||||
|
||||
Directory directory = IFDs.getDirectory(0);
|
||||
Entry photoshop = directory.getEntryById(TIFF.TAG_PHOTOSHOP);
|
||||
|
||||
if (photoshop != null) {
|
||||
return buildPathFromPhotoshopResources(new ByteArrayImageInputStream((byte[]) photoshop.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown file format, or no path found
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int readMagic(final ImageInputStream stream) throws IOException {
|
||||
stream.mark();
|
||||
|
||||
try {
|
||||
return stream.readInt();
|
||||
}
|
||||
finally {
|
||||
stream.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private static Path2D buildPathFromPhotoshopResources(final ImageInputStream stream) throws IOException {
|
||||
Directory resourceBlocks = new PSDReader().read(stream);
|
||||
|
||||
if (AdobePathBuilder.DEBUG) {
|
||||
System.out.println("resourceBlocks: " + resourceBlocks);
|
||||
}
|
||||
|
||||
Entry resourceBlock = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH);
|
||||
|
||||
if (resourceBlock != null) {
|
||||
return new AdobePathBuilder((byte[]) resourceBlock.getValue()).path();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the clipping path to the given image.
|
||||
* All pixels outside the path will be transparent.
|
||||
*
|
||||
* @param clip the clipping path, not {@code null}
|
||||
* @param image the image to clip, not {@code null}
|
||||
* @return the clipped image.
|
||||
*
|
||||
* @throws java.lang.IllegalArgumentException if {@code clip} or {@code image} is {@code null}.
|
||||
*/
|
||||
public static BufferedImage applyClippingPath(final Shape clip, final BufferedImage image) {
|
||||
return applyClippingPath(clip, notNull(image, "image"), new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the clipping path to the given image.
|
||||
* Client code may decide the type of the {@code destination} image.
|
||||
* The {@code destination} image is assumed to be fully transparent,
|
||||
* and have same dimensions as {@code image}.
|
||||
* All pixels outside the path will be transparent.
|
||||
*
|
||||
* @param clip the clipping path, not {@code null}.
|
||||
* @param image the image to clip, not {@code null}.
|
||||
* @param destination the destination image, may not be {@code null} or same instance as {@code image}.
|
||||
* @return the clipped image.
|
||||
*
|
||||
* @throws java.lang.IllegalArgumentException if {@code clip}, {@code image} or {@code destination} is {@code null},
|
||||
* or if {@code destination} is the same instance as {@code image}.
|
||||
*/
|
||||
public static BufferedImage applyClippingPath(final Shape clip, final BufferedImage image, final BufferedImage destination) {
|
||||
notNull(clip, "clip");
|
||||
notNull(image, "image");
|
||||
isTrue(destination != null && destination != image, "destination may not be null or same instance as image");
|
||||
|
||||
Graphics2D g = destination.createGraphics();
|
||||
|
||||
try {
|
||||
AffineTransform originalTransform = g.getTransform();
|
||||
|
||||
// Fill the clip shape, with antialias, scaled up to the image's size
|
||||
g.scale(image.getWidth(), image.getHeight());
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.fill(clip);
|
||||
|
||||
// Draw the image inside the clip shape
|
||||
g.setTransform(originalTransform);
|
||||
g.setComposite(AlphaComposite.SrcIn);
|
||||
g.drawImage(image, 0, 0, null);
|
||||
}
|
||||
finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the clipping path from the given input stream, if any,
|
||||
* and applies it to the first image in the stream.
|
||||
* If no path was found, the image is returned without any clipping.
|
||||
* Supports PSD, JPEG and TIFF as container formats for Photoshop resources.
|
||||
*
|
||||
* @param stream the stream to read from, not {@code null}
|
||||
* @return the clipped image
|
||||
*
|
||||
* @throws IOException if a general I/O exception occurs during reading.
|
||||
* @throws javax.imageio.IIOException if the input contains a bad image or path data.
|
||||
* @throws java.lang.IllegalArgumentException is {@code stream} is {@code null}.
|
||||
*/
|
||||
public static BufferedImage readClipped(final ImageInputStream stream) throws IOException {
|
||||
Shape clip = readPath(stream);
|
||||
|
||||
stream.seek(0);
|
||||
BufferedImage image = ImageIO.read(stream);
|
||||
|
||||
if (clip == null) {
|
||||
return image;
|
||||
}
|
||||
|
||||
return applyClippingPath(clip, image);
|
||||
}
|
||||
|
||||
// Test code
|
||||
public static void main(final String[] args) throws IOException, InterruptedException {
|
||||
BufferedImage destination = readClipped(ImageIO.createImageInputStream(new File(args[0])));
|
||||
|
||||
File tempFile = File.createTempFile("clipped-", ".png");
|
||||
tempFile.deleteOnExit();
|
||||
ImageIO.write(destination, "PNG", tempFile);
|
||||
|
||||
Desktop.getDesktop().open(tempFile);
|
||||
|
||||
Thread.sleep(3000l);
|
||||
|
||||
if (!tempFile.delete()) {
|
||||
System.err.printf("%s not deleted\n", tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
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 AdobePathBuilderTest {
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullBytes() {
|
||||
new AdobePathBuilder((byte[]) null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNull() {
|
||||
new AdobePathBuilder((DataInput) null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateEmpty() {
|
||||
new AdobePathBuilder(new byte[0]);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateShortPath() {
|
||||
new AdobePathBuilder(new byte[3]);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateImpossiblePath() {
|
||||
new AdobePathBuilder(new byte[7]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
new AdobePathBuilder(new byte[52]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoPath() throws IOException {
|
||||
Path2D path = new AdobePathBuilder(new byte[26]).path();
|
||||
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 AdobePathBuilder(data).path();
|
||||
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 AdobePathBuilder(data).path();
|
||||
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 AdobePathBuilder(data).path();
|
||||
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 AdobePathBuilder(data).path();
|
||||
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 AdobePathBuilder(data).path();
|
||||
|
||||
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 AdobePathBuilder(data).path();
|
||||
|
||||
assertNotNull(path);
|
||||
assertPathEquals(path, readExpectedPath("/ser/multiple-clips.ser"));
|
||||
}
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
package com.twelvemonkeys.imageio.path;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* AdobePathSegmentTest.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: AdobePathSegmentTest.java,v 1.0 13/12/14 harald.kuhr Exp$
|
||||
*/
|
||||
public class AdobePathSegmentTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateBadSelectorNegative() {
|
||||
new AdobePathSegment(-1, 1);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateBadSelector() {
|
||||
new AdobePathSegment(9, 2);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateOpenLengthRecordNegative() {
|
||||
new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, -1);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateOpenLengthRecord() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42);
|
||||
|
||||
assertEquals(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, segment.selector);
|
||||
assertEquals(42, segment.length);
|
||||
assertEquals(-1, segment.cppx, 0);
|
||||
assertEquals(-1, segment.cppy, 0);
|
||||
assertEquals(-1, segment.apx, 0);
|
||||
assertEquals(-1, segment.apy, 0);
|
||||
assertEquals(-1, segment.cplx, 0);
|
||||
assertEquals(-1, segment.cply, 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateClosedLengthRecordNegative() {
|
||||
new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, -42);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateClosedLengthRecord() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 27);
|
||||
|
||||
assertEquals(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, segment.selector);
|
||||
assertEquals(27, segment.length);
|
||||
assertEquals(-1, segment.cppx, 0);
|
||||
assertEquals(-1, segment.cppy, 0);
|
||||
assertEquals(-1, segment.apx, 0);
|
||||
assertEquals(-1, segment.apy, 0);
|
||||
assertEquals(-1, segment.cplx, 0);
|
||||
assertEquals(-1, segment.cply, 0);
|
||||
}
|
||||
|
||||
/// Open subpath
|
||||
|
||||
@Test
|
||||
public void testCreateOpenLinkedRecord() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
|
||||
|
||||
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, segment.selector);
|
||||
assertEquals(-1, segment.length);
|
||||
assertEquals(.5, segment.cppx, 0);
|
||||
assertEquals(.5, segment.cppy, 0);
|
||||
assertEquals(0, segment.apx, 0);
|
||||
assertEquals(0, segment.apy, 0);
|
||||
assertEquals(1, segment.cplx, 0);
|
||||
assertEquals(1, segment.cply, 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateOpenLinkedRecordBad() {
|
||||
new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, 44);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateOpenLinkedRecordNegative() {
|
||||
new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, -.5, -.5, 0, 0, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateOpenUnlinkedRecord() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
|
||||
|
||||
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, segment.selector);
|
||||
assertEquals(-1, segment.length);
|
||||
assertEquals(.5, segment.cppx, 0);
|
||||
assertEquals(.5, segment.cppy, 0);
|
||||
assertEquals(0, segment.apx, 0);
|
||||
assertEquals(0, segment.apy, 0);
|
||||
assertEquals(1, segment.cplx, 0);
|
||||
assertEquals(1, segment.cply, 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateOpenUnlinkedRecordBad() {
|
||||
new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, 44);
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateOpenUnlinkedRecordNegative() {
|
||||
new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, -.5, -.5, 0, 0, 1, 1);
|
||||
}
|
||||
|
||||
/// Closed subpath
|
||||
|
||||
@Test
|
||||
public void testCreateClosedLinkedRecord() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
|
||||
|
||||
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, segment.selector);
|
||||
assertEquals(-1, segment.length);
|
||||
assertEquals(.5, segment.cppx, 0);
|
||||
assertEquals(.5, segment.cppy, 0);
|
||||
assertEquals(0, segment.apx, 0);
|
||||
assertEquals(0, segment.apy, 0);
|
||||
assertEquals(1, segment.cplx, 0);
|
||||
assertEquals(1, segment.cply, 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateClosedLinkedRecordBad() {
|
||||
new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 44);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateClosedLinkedRecordNegative() {
|
||||
new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, -.5, -.5, 0, 0, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateClosedUnlinkedRecord() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
|
||||
|
||||
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, segment.selector);
|
||||
assertEquals(-1, segment.length);
|
||||
assertEquals(.5, segment.cppx, 0);
|
||||
assertEquals(.5, segment.cppy, 0);
|
||||
assertEquals(0, segment.apx, 0);
|
||||
assertEquals(0, segment.apy, 0);
|
||||
assertEquals(1, segment.cplx, 0);
|
||||
assertEquals(1, segment.cply, 0);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateClosedUnlinkedRecordBad() {
|
||||
new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, 44);
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateClosedUnlinkedRecordNegative() {
|
||||
new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, -.5, -.5, 0, 0, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToStringRule() {
|
||||
String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD, 2).toString();
|
||||
assertTrue(string, string.startsWith("Rule"));
|
||||
assertTrue(string, string.contains("Initial"));
|
||||
assertTrue(string, string.contains("fill"));
|
||||
assertTrue(string, string.contains("rule=2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToStringLength() {
|
||||
String string = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2).toString();
|
||||
assertTrue(string, string.startsWith("Len"));
|
||||
assertTrue(string, string.contains("Closed"));
|
||||
assertTrue(string, string.contains("subpath"));
|
||||
assertTrue(string, string.contains("totalPoints=2"));
|
||||
|
||||
string = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42).toString();
|
||||
assertTrue(string, string.startsWith("Len"));
|
||||
assertTrue(string, string.contains("Open"));
|
||||
assertTrue(string, string.contains("subpath"));
|
||||
assertTrue(string, string.contains("totalPoints=42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToStringOther() {
|
||||
String string = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0).toString();
|
||||
assertTrue(string, string.startsWith("Pt"));
|
||||
assertTrue(string, string.contains("Open"));
|
||||
assertTrue(string, string.contains("Bezier"));
|
||||
assertTrue(string, string.contains("linked"));
|
||||
|
||||
string = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0).toString();
|
||||
assertTrue(string, string.startsWith("Pt"));
|
||||
assertTrue(string, string.contains("Closed"));
|
||||
assertTrue(string, string.contains("Bezier"));
|
||||
assertTrue(string, string.contains("linked"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsLength() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2);
|
||||
assertEquals(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2), segment);
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 3).equals(segment));
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 2).equals(segment));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsOther() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0);
|
||||
assertEquals(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0), segment);
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, 0, 0, 1, 1, 0, 0).equals(segment));
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 0, 1, 1, 0, 0).equals(segment));
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, 0, 0.1, 1, 1, 0, 0).equals(segment));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHashCodeLength() {
|
||||
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2);
|
||||
assertEquals(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 2).hashCode(), segment.hashCode());
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 3).hashCode() == segment.hashCode());
|
||||
assertFalse(new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 2).hashCode() == segment.hashCode());
|
||||
}
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
package com.twelvemonkeys.imageio.path;
|
||||
|
||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
||||
import com.twelvemonkeys.imageio.stream.URLImageInputStreamSpi;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.GeneralPath;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.PathIterator;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* PathsTest.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: PathsTest.java,v 1.0 12/12/14 harald.kuhr Exp$
|
||||
*/
|
||||
public class PathsTest {
|
||||
static {
|
||||
IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadPathNull() throws IOException {
|
||||
Paths.readPath(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPathUnknown() throws IOException {
|
||||
assertNull(Paths.readPath(new ByteArrayImageInputStream(new byte[42])));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrapeJPEG() throws IOException {
|
||||
ImageInputStream stream = resourceAsIIOStream("/jpeg/grape_with_path.jpg");
|
||||
|
||||
Path2D path = Paths.readPath(stream);
|
||||
|
||||
assertNotNull(path);
|
||||
assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrapePSD() throws IOException {
|
||||
ImageInputStream stream = resourceAsIIOStream("/psd/grape_with_path.psd");
|
||||
|
||||
Path2D path = Paths.readPath(stream);
|
||||
|
||||
assertNotNull(path);
|
||||
assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrapeTIFF() throws IOException {
|
||||
ImageInputStream stream = resourceAsIIOStream("/tiff/little-endian-grape_with_path.tif");
|
||||
|
||||
Path2D path = Paths.readPath(stream);
|
||||
|
||||
assertNotNull(path);
|
||||
assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleTIFF() throws IOException {
|
||||
ImageInputStream stream = resourceAsIIOStream("/tiff/big-endian-multiple-clips.tif");
|
||||
|
||||
Shape path = Paths.readPath(stream);
|
||||
|
||||
assertNotNull(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrape8BIM() throws IOException {
|
||||
ImageInputStream stream = resourceAsIIOStream("/psd/grape_with_path.psd");
|
||||
|
||||
// PSD image resources from position 34, length 32598
|
||||
stream.seek(34);
|
||||
stream = new SubImageInputStream(stream, 32598);
|
||||
|
||||
Path2D path = Paths.readPath(stream);
|
||||
|
||||
assertNotNull(path);
|
||||
assertPathEquals(readExpectedPath("/ser/grape-path.ser"), path);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testApplyClippingPathNullPath() throws IOException {
|
||||
Paths.applyClippingPath(null, new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testApplyClippingPathNullSource() throws IOException {
|
||||
Paths.applyClippingPath(new GeneralPath(), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyClippingPath() throws IOException {
|
||||
BufferedImage source = new BufferedImage(20, 20, BufferedImage.TYPE_3BYTE_BGR);
|
||||
|
||||
Path2D path = readExpectedPath("/ser/grape-path.ser");
|
||||
|
||||
BufferedImage image = Paths.applyClippingPath(path, source);
|
||||
|
||||
assertNotNull(image);
|
||||
// Same dimensions as original
|
||||
assertEquals(source.getWidth(), image.getWidth());
|
||||
assertEquals(source.getHeight(), image.getHeight());
|
||||
// Transparent
|
||||
assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
|
||||
|
||||
// Corners (at least) should be transparent
|
||||
assertEquals(0, image.getRGB(0, 0));
|
||||
assertEquals(0, image.getRGB(source.getWidth() - 1, 0));
|
||||
assertEquals(0, image.getRGB(0, source.getHeight() - 1));
|
||||
assertEquals(0, image.getRGB(source.getWidth() - 1, source.getHeight() - 1));
|
||||
|
||||
// Center opaque
|
||||
assertEquals(0xff, image.getRGB(source.getWidth() / 2, source.getHeight() / 2) >>> 24);
|
||||
|
||||
// TODO: Mor sophisticated test that tests all pixels outside path...
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testApplyClippingPathNullDestination() throws IOException {
|
||||
Paths.applyClippingPath(new GeneralPath(), new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyClippingPathCustomDestination() throws IOException {
|
||||
BufferedImage source = new BufferedImage(20, 20, BufferedImage.TYPE_3BYTE_BGR);
|
||||
|
||||
Path2D path = readExpectedPath("/ser/grape-path.ser");
|
||||
|
||||
// Destination is intentionally larger than source
|
||||
BufferedImage destination = new BufferedImage(30, 30, BufferedImage.TYPE_4BYTE_ABGR);
|
||||
BufferedImage image = Paths.applyClippingPath(path, source, destination);
|
||||
|
||||
assertSame(destination, image);
|
||||
|
||||
// Corners (at least) should be transparent
|
||||
assertEquals(0, image.getRGB(0, 0));
|
||||
assertEquals(0, image.getRGB(image.getWidth() - 1, 0));
|
||||
assertEquals(0, image.getRGB(0, image.getHeight() - 1));
|
||||
assertEquals(0, image.getRGB(image.getWidth() - 1, image.getHeight() - 1));
|
||||
|
||||
// "inner" corners
|
||||
assertEquals(0, image.getRGB(source.getWidth() - 1, 0));
|
||||
assertEquals(0, image.getRGB(0, source.getHeight() - 1));
|
||||
assertEquals(0, image.getRGB(source.getWidth() - 1, source.getHeight() - 1));
|
||||
|
||||
// Center opaque
|
||||
assertEquals(0xff, image.getRGB(source.getWidth() / 2, source.getHeight() / 2) >>> 24);
|
||||
|
||||
// TODO: Mor sophisticated test that tests all pixels outside path...
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testReadClippedNull() throws IOException {
|
||||
Paths.readClipped(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadClipped() throws IOException {
|
||||
BufferedImage image = Paths.readClipped(resourceAsIIOStream("/jpeg/grape_with_path.jpg"));
|
||||
|
||||
assertNotNull(image);
|
||||
// Same dimensions as original
|
||||
assertEquals(857, image.getWidth());
|
||||
assertEquals(1800, image.getHeight());
|
||||
// Transparent
|
||||
assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
|
||||
|
||||
// Corners (at least) should be transparent
|
||||
assertEquals(0, image.getRGB(0, 0));
|
||||
assertEquals(0, image.getRGB(image.getWidth() - 1, 0));
|
||||
assertEquals(0, image.getRGB(0, image.getHeight() - 1));
|
||||
assertEquals(0, image.getRGB(image.getWidth() - 1, image.getHeight() - 1));
|
||||
|
||||
// Center opaque
|
||||
assertEquals(0xff, image.getRGB(image.getWidth() / 2, image.getHeight() / 2) >>> 24);
|
||||
|
||||
// TODO: Mor sophisticated test that tests all pixels outside path...
|
||||
}
|
||||
|
||||
// TODO: Test read image without path, as no-op
|
||||
|
||||
static ImageInputStream resourceAsIIOStream(String name) throws IOException {
|
||||
return ImageIO.createImageInputStream(PathsTest.class.getResource(name));
|
||||
}
|
||||
|
||||
static Path2D readExpectedPath(final String resource) throws IOException {
|
||||
ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource));
|
||||
|
||||
try {
|
||||
return (Path2D) ois.readObject();
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
finally {
|
||||
ois.close();
|
||||
}
|
||||
}
|
||||
|
||||
static void assertPathEquals(final Path2D expectedPath, final Path2D actualPath) {
|
||||
PathIterator expectedIterator = expectedPath.getPathIterator(null);
|
||||
PathIterator actualIterator = actualPath.getPathIterator(null);
|
||||
float[] expectedCoords = new float[6];
|
||||
float[] actualCoords = new float[6];
|
||||
|
||||
while(!actualIterator.isDone()) {
|
||||
int expectedType = expectedIterator.currentSegment(expectedCoords);
|
||||
int actualType = actualIterator.currentSegment(actualCoords);
|
||||
|
||||
assertEquals(expectedType, actualType);
|
||||
assertArrayEquals(expectedCoords, actualCoords, 0);
|
||||
|
||||
actualIterator.next();
|
||||
expectedIterator.next();
|
||||
}
|
||||
}
|
||||
}
|
1
imageio/imageio-clippath/src/test/resources/images.txt
Normal file
1
imageio/imageio-clippath/src/test/resources/images.txt
Normal file
@ -0,0 +1 @@
|
||||
Sample images kindly provided by itemMaster LLC (https://www.itemmaster.com/).
|
Binary file not shown.
After Width: | Height: | Size: 329 KiB |
BIN
imageio/imageio-clippath/src/test/resources/jpeg/single-clip.jpg
Normal file
BIN
imageio/imageio-clippath/src/test/resources/jpeg/single-clip.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
Binary file not shown.
BIN
imageio/imageio-clippath/src/test/resources/ser/grape-path.ser
Normal file
BIN
imageio/imageio-clippath/src/test/resources/ser/grape-path.ser
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -167,9 +167,28 @@ public interface TIFF {
|
||||
int TAG_OLD_SUBFILE_TYPE = 255; // Deprecated NO NOT WRITE!
|
||||
int TAG_SUB_IFD = 330;
|
||||
|
||||
/**
|
||||
* XMP record.
|
||||
* @see com.twelvemonkeys.imageio.metadata.xmp.XMP
|
||||
*/
|
||||
int TAG_XMP = 700;
|
||||
|
||||
/**
|
||||
* IPTC record.
|
||||
* @see com.twelvemonkeys.imageio.metadata.iptc.IPTC
|
||||
*/
|
||||
int TAG_IPTC = 33723;
|
||||
|
||||
/**
|
||||
* Photoshop image resources.
|
||||
* @see com.twelvemonkeys.imageio.metadata.psd.PSD
|
||||
*/
|
||||
int TAG_PHOTOSHOP = 34377;
|
||||
|
||||
/**
|
||||
* ICC Color Profile.
|
||||
* @see java.awt.color.ICC_Profile
|
||||
*/
|
||||
int TAG_ICC_PROFILE = 34675;
|
||||
|
||||
// Microsoft Office Document Imaging (MODI)
|
||||
|
@ -36,6 +36,9 @@ package com.twelvemonkeys.imageio.metadata.psd;
|
||||
* @version $Id: PSD.java,v 1.0 24.01.12 16:51 haraldk Exp$
|
||||
*/
|
||||
public interface PSD {
|
||||
/** PSD 2+ Native format (.PSD) identifier "8BPS" */
|
||||
int SIGNATURE_8BPS = ('8' << 24) + ('B' << 16) + ('P' << 8) + 'S';
|
||||
|
||||
/** PSD image resource marker "8BIM". */
|
||||
int RESOURCE_TYPE = ('8' << 24) + ('B' << 16) + ('I' << 8) + 'M';
|
||||
|
||||
@ -44,4 +47,7 @@ public interface PSD {
|
||||
|
||||
/** ICC profile image resource id. */
|
||||
int RES_ICC_PROFILE = 0x040f;
|
||||
|
||||
/** PSD Path resource id. */
|
||||
int RES_CLIPPING_PATH = 0x07d0;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
<!-- Support -->
|
||||
<module>imageio-core</module>
|
||||
<module>imageio-metadata</module>
|
||||
<module>imageio-clippath</module>
|
||||
|
||||
<!-- Stand-alone readers/writers -->
|
||||
<module>imageio-bmp</module>
|
||||
|
Loading…
x
Reference in New Issue
Block a user