TMI-60: Support for clip paths in formats containing PSD resources

This commit is contained in:
Harald Kuhr
2014-12-16 11:38:24 +01:00
parent c2e9b585ff
commit 77e6600605
20 changed files with 1374 additions and 0 deletions
@@ -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();
}
}
}
@@ -0,0 +1 @@
Sample images kindly provided by itemMaster LLC (https://www.itemmaster.com/).
Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB