photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
if (!photoshop.isEmpty()) {
- return buildPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data()));
+ return readPathFromPhotoshopResources(new MemoryCacheImageInputStream(photoshop.get(0).data()));
}
}
else if (magic >>> 16 == TIFF.BYTE_ORDER_MARK_BIG_ENDIAN && (magic & 0xffff) == TIFF.TIFF_MAGIC
@@ -137,7 +143,7 @@ public final class Paths {
Entry photoshop = directory.getEntryById(TIFF.TAG_PHOTOSHOP);
if (photoshop != null) {
- return buildPathFromPhotoshopResources(new ByteArrayImageInputStream((byte[]) photoshop.getValue()));
+ return readPathFromPhotoshopResources(new ByteArrayImageInputStream((byte[]) photoshop.getValue()));
}
}
@@ -156,17 +162,17 @@ public final class Paths {
}
}
- private static Path2D buildPathFromPhotoshopResources(final ImageInputStream stream) throws IOException {
+ private static Path2D readPathFromPhotoshopResources(final ImageInputStream stream) throws IOException {
Directory resourceBlocks = new PSDReader().read(stream);
- if (AdobePathBuilder.DEBUG) {
+ if (AdobePathReader.DEBUG) {
System.out.println("resourceBlocks: " + resourceBlocks);
}
- Entry resourceBlock = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH);
+ Entry pathResource = resourceBlocks.getEntryById(PSD.RES_CLIPPING_PATH);
- if (resourceBlock != null) {
- return new AdobePathBuilder((byte[]) resourceBlock.getValue()).path();
+ if (pathResource != null) {
+ return new AdobePathReader((byte[]) pathResource.getValue()).readPath();
}
return null;
@@ -254,9 +260,149 @@ public final class Paths {
return applyClippingPath(clip, image);
}
+ /**
+ * Writes the image along with a clipping path resource, in the given format, to the supplied output.
+ * The image is written to the
+ * {@code ImageOutputStream} starting at the current stream
+ * pointer, overwriting existing stream data from that point
+ * forward, if present.
+ *
+ * Note: As {@link ImageIO#write(RenderedImage, String, ImageOutputStream)}, this method does
+ * not close the output stream.
+ * It is the responsibility of the caller to close the stream, if desired.
+ *
+ *
+ * Implementation note: Only JPEG (using the "javax_imageio_jpeg_image_1.0" metadata format) and
+ * TIFF (using the "javax_imageio_tiff_image_1.0" or "com_sun_media_imageio_plugins_tiff_image_1.0" metadata formats)
+ * formats are currently supported.
+ *
+ *
+ * @param image the image to be written, may not be {@code null}.
+ * @param clipPath the clip path, may not be {@code null}.
+ * @param formatName the informal format name, may not be {@code null}.
+ * @param output the stream to write to, may not be {@code null}.
+ *
+ * @return {@code true} if the image was written,
+ * otherwise {@code false} (ie. no writer was found for the specified format).
+ *
+ * @exception IllegalArgumentException if any parameter is {@code null}.
+ * @exception IOException if an error occurs during writing.
+ */
+ public static boolean writeClipped(final RenderedImage image, Shape clipPath, final String formatName, final ImageOutputStream output) throws IOException {
+ if (image == null) {
+ throw new IllegalArgumentException("image == null!");
+ }
+ if (formatName == null) {
+ throw new IllegalArgumentException("formatName == null!");
+ }
+ if (output == null) {
+ throw new IllegalArgumentException("output == null!");
+ }
+
+ ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image);
+ Iterator writers = ImageIO.getImageWriters(type, formatName);
+
+ if (writers.hasNext()) {
+ ImageWriter writer = writers.next();
+
+ ImageWriteParam param = writer.getDefaultWriteParam();
+ IIOMetadata metadata = writer.getDefaultImageMetadata(type, param);
+ List metadataFormats = asList(metadata.getMetadataFormatNames());
+
+ byte[] pathResource = new AdobePathWriter(clipPath).writePathResource(PSD.RES_CLIPPING_PATH);
+
+ if (metadataFormats.contains("javax_imageio_tiff_image_1.0") || metadataFormats.contains("com_sun_media_imageio_plugins_tiff_image_1.0")) {
+ param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+ param.setCompressionType("Deflate");
+
+ // Check if the format is that of the bundled TIFF writer, otherwise use JAI format
+ String metadataFormat = metadataFormats.contains("javax_imageio_tiff_image_1.0")
+ ? "javax_imageio_tiff_image_1.0"
+ : "com_sun_media_imageio_plugins_tiff_image_1.0"; // Fails in mergeTree, if not supported
+ IIOMetadataNode root = new IIOMetadataNode(metadataFormat);
+ IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD");
+
+ IIOMetadataNode pathField = new IIOMetadataNode("TIFFField");
+ pathField.setAttribute("number", String.valueOf(TIFF.TAG_PHOTOSHOP));
+ IIOMetadataNode pathValue = new IIOMetadataNode("TIFFUndefined"); // Use undefined for simplicity, could also use bytes
+ pathValue.setAttribute("value", arrayAsString(pathResource));
+
+ pathField.appendChild(pathValue);
+ ifd.appendChild(pathField);
+ root.appendChild(ifd);
+
+ metadata.mergeTree(metadataFormat, root);
+
+ writer.setOutput(output);
+ writer.write(null, new IIOImage(image, null, metadata), param);
+
+ return true;
+ }
+ else if (metadataFormats.contains("javax_imageio_jpeg_image_1.0")) {
+ String metadataFormat = "javax_imageio_jpeg_image_1.0";
+ IIOMetadataNode root = new IIOMetadataNode(metadataFormat);
+
+ root.appendChild(new IIOMetadataNode("JPEGvariety"));
+
+ IIOMetadataNode sequence = new IIOMetadataNode("markerSequence");
+
+ // App13/Photshop 3.0
+ IIOMetadataNode unknown = new IIOMetadataNode("unknown");
+ unknown.setAttribute("MarkerTag", Integer.toString(JPEG.APP13 & 0xFF));
+
+ byte[] identfier = "Photoshop 3.0".getBytes(StandardCharsets.US_ASCII);
+ byte[] data = new byte[identfier.length + 1 + pathResource.length];
+ System.arraycopy(identfier, 0, data, 0, identfier.length);
+ System.arraycopy(pathResource, 0, data, identfier.length + 1, pathResource.length);
+
+ unknown.setUserObject(data);
+
+ sequence.appendChild(unknown);
+ root.appendChild(sequence);
+
+ metadata.mergeTree(metadataFormat, root);
+
+ writer.setOutput(output);
+ writer.write(null, new IIOImage(image, null, metadata), param);
+
+ return true;
+ }
+ // TODO: Else if PSD... Requires PSD write + new metadata format...
+ }
+
+ return false;
+ }
+
+ private static String arrayAsString(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; ; i++) {
+ builder.append(bytes[i]);
+
+ if (i == bytes.length - 1) {
+ return builder.toString();
+ }
+
+ builder.append(","); // NOTE: The javax_imageio_tiff_image_1.0 format does not allow whitespace here...
+ }
+ }
+
// Test code
public static void main(final String[] args) throws IOException, InterruptedException {
- BufferedImage destination = readClipped(ImageIO.createImageInputStream(new File(args[0])));
+ BufferedImage destination;
+ if (args.length == 1) {
+ // Embedded path
+ destination = readClipped(ImageIO.createImageInputStream(new File(args[0])));
+ }
+ else {
+ // Separate path and image
+ try (ImageInputStream input = ImageIO.createImageInputStream(new File(args[1]))) {
+ destination = applyClippingPath(readPath(input), ImageIO.read(new File(args[0])));
+ }
+ }
File tempFile = File.createTempFile("clipped-", ".png");
tempFile.deleteOnExit();
@@ -270,5 +416,4 @@ public final class Paths {
System.err.printf("%s not deleted\n", tempFile);
}
}
-
}
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java
index 1e917a86..ddd05dc5 100644
--- a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathBuilderTest.java
@@ -43,6 +43,7 @@ import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals;
import static com.twelvemonkeys.imageio.path.PathsTest.readExpectedPath;
import static org.junit.Assert.assertNotNull;
+@SuppressWarnings("deprecation")
public class AdobePathBuilderTest {
@Test(expected = IllegalArgumentException.class)
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java
new file mode 100644
index 00000000..2837bbe9
--- /dev/null
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathReaderTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2020 Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+package com.twelvemonkeys.imageio.path;
+
+import org.junit.Test;
+
+import javax.imageio.IIOException;
+import javax.imageio.stream.ImageInputStream;
+import java.awt.geom.Path2D;
+import java.io.DataInput;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals;
+import static com.twelvemonkeys.imageio.path.PathsTest.readExpectedPath;
+import static org.junit.Assert.assertNotNull;
+
+public class AdobePathReaderTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateNullBytes() {
+ new AdobePathReader((byte[]) null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateNull() {
+ new AdobePathReader((DataInput) null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateEmpty() {
+ new AdobePathReader(new byte[0]);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateShortPath() {
+ new AdobePathReader(new byte[3]);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateImpossiblePath() {
+ new AdobePathReader(new byte[7]);
+ }
+
+ @Test
+ public void testCreate() {
+ new AdobePathReader(new byte[52]);
+ }
+
+ @Test
+ public void testNoPath() throws IOException {
+ Path2D path = new AdobePathReader(new byte[26]).readPath();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testShortPath() throws IOException {
+ byte[] data = new byte[26];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD);
+ buffer.putShort((short) 1);
+
+ Path2D path = new AdobePathReader(data).readPath();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testShortPathToo() throws IOException {
+ byte[] data = new byte[52];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD);
+ buffer.putShort((short) 2);
+ buffer.position(buffer.position() + 22);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+
+ Path2D path = new AdobePathReader(data).readPath();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testLongPath() throws IOException {
+ byte[] data = new byte[78];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD);
+ buffer.putShort((short) 1);
+ buffer.position(buffer.position() + 22);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+ buffer.position(buffer.position() + 24);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+
+ Path2D path = new AdobePathReader(data).readPath();
+ assertNotNull(path);
+ }
+
+ @Test(expected = IIOException.class)
+ public void testPathMissingLength() throws IOException {
+ byte[] data = new byte[26];
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.putShort((short) AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED);
+
+ Path2D path = new AdobePathReader(data).readPath();
+ assertNotNull(path);
+ }
+
+ @Test
+ public void testSimplePath() throws IOException {
+ // We'll read this from a real file, with hardcoded offsets for simplicity
+ // PSD IRB: offset: 34, length: 32598
+ // Clipping path: offset: 31146, length: 1248
+ ImageInputStream stream = PathsTest.resourceAsIIOStream("/psd/grape_with_path.psd");
+ stream.seek(34 + 31146);
+ byte[] data = new byte[1248];
+ stream.readFully(data);
+
+ Path2D path = new AdobePathReader(data).readPath();
+
+ assertNotNull(path);
+ assertPathEquals(path, readExpectedPath("/ser/grape-path.ser"));
+ }
+
+ @Test
+ public void testComplexPath() throws IOException {
+ // We'll read this from a real file, with hardcoded offsets for simplicity
+ // PSD IRB: offset: 16970, length: 11152
+ // Clipping path: offset: 9250, length: 1534
+ ImageInputStream stream = PathsTest.resourceAsIIOStream("/tiff/big-endian-multiple-clips.tif");
+ stream.seek(16970 + 9250);
+ byte[] data = new byte[1534];
+ stream.readFully(data);
+
+ Path2D path = new AdobePathReader(data).readPath();
+
+ assertNotNull(path);
+ assertPathEquals(path, readExpectedPath("/ser/multiple-clips.ser"));
+ }
+}
\ No newline at end of file
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java
index c42aee6a..7b4827e4 100644
--- a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathSegmentTest.java
@@ -63,7 +63,7 @@ public class AdobePathSegmentTest {
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42);
assertEquals(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, segment.selector);
- assertEquals(42, segment.length);
+ assertEquals(42, segment.lengthOrRule);
assertEquals(-1, segment.cppx, 0);
assertEquals(-1, segment.cppy, 0);
assertEquals(-1, segment.apx, 0);
@@ -82,7 +82,7 @@ public class AdobePathSegmentTest {
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, 27);
assertEquals(AdobePathSegment.CLOSED_SUBPATH_LENGTH_RECORD, segment.selector);
- assertEquals(27, segment.length);
+ assertEquals(27, segment.lengthOrRule);
assertEquals(-1, segment.cppx, 0);
assertEquals(-1, segment.cppy, 0);
assertEquals(-1, segment.apx, 0);
@@ -98,7 +98,7 @@ public class AdobePathSegmentTest {
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_LINKED, segment.selector);
- assertEquals(-1, segment.length);
+ assertEquals(-1, segment.lengthOrRule);
assertEquals(.5, segment.cppx, 0);
assertEquals(.5, segment.cppy, 0);
assertEquals(0, segment.apx, 0);
@@ -122,7 +122,7 @@ public class AdobePathSegmentTest {
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
assertEquals(AdobePathSegment.OPEN_SUBPATH_BEZIER_UNLINKED, segment.selector);
- assertEquals(-1, segment.length);
+ assertEquals(-1, segment.lengthOrRule);
assertEquals(.5, segment.cppx, 0);
assertEquals(.5, segment.cppy, 0);
assertEquals(0, segment.apx, 0);
@@ -149,7 +149,7 @@ public class AdobePathSegmentTest {
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, .5, .5, 0, 0, 1, 1);
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_LINKED, segment.selector);
- assertEquals(-1, segment.length);
+ assertEquals(-1, segment.lengthOrRule);
assertEquals(.5, segment.cppx, 0);
assertEquals(.5, segment.cppy, 0);
assertEquals(0, segment.apx, 0);
@@ -173,7 +173,7 @@ public class AdobePathSegmentTest {
AdobePathSegment segment = new AdobePathSegment(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, .5, .5, 0, 0, 1, 1);
assertEquals(AdobePathSegment.CLOSED_SUBPATH_BEZIER_UNLINKED, segment.selector);
- assertEquals(-1, segment.length);
+ assertEquals(-1, segment.lengthOrRule);
assertEquals(.5, segment.cppx, 0);
assertEquals(.5, segment.cppy, 0);
assertEquals(0, segment.apx, 0);
@@ -195,11 +195,11 @@ public class AdobePathSegmentTest {
@Test
public void testToStringRule() {
- String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD, 2).toString();
+ String string = new AdobePathSegment(AdobePathSegment.INITIAL_FILL_RULE_RECORD, 0).toString();
assertTrue(string, string.startsWith("Rule"));
assertTrue(string, string.contains("Initial"));
assertTrue(string, string.contains("fill"));
- assertTrue(string, string.contains("rule=2"));
+ assertTrue(string, string.contains("rule=0"));
}
@Test
@@ -208,13 +208,13 @@ public class AdobePathSegmentTest {
assertTrue(string, string.startsWith("Len"));
assertTrue(string, string.contains("Closed"));
assertTrue(string, string.contains("subpath"));
- assertTrue(string, string.contains("totalPoints=2"));
+ assertTrue(string, string.contains("length=2"));
string = new AdobePathSegment(AdobePathSegment.OPEN_SUBPATH_LENGTH_RECORD, 42).toString();
assertTrue(string, string.startsWith("Len"));
assertTrue(string, string.contains("Open"));
assertTrue(string, string.contains("subpath"));
- assertTrue(string, string.contains("totalPoints=42"));
+ assertTrue(string, string.contains("length=42"));
}
@Test
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java
new file mode 100644
index 00000000..5179ed1e
--- /dev/null
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/AdobePathWriterTest.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2020 Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.path;
+
+import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
+import org.junit.Test;
+
+import javax.imageio.ImageIO;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import java.awt.*;
+import java.awt.geom.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static com.twelvemonkeys.imageio.path.AdobePathSegment.*;
+import static com.twelvemonkeys.imageio.path.PathsTest.assertPathEquals;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * AdobePathWriterTest.
+ *
+ * @author Harald Kuhr
+ * @author last modified by haraldk: harald.kuhr$
+ * @version : AdobePathWriterTest.java,v 1.0 2020-01-02 harald.kuhr Exp$
+ */
+public class AdobePathWriterTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateWriterNull() {
+ new AdobePathWriter(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateWriterInvalid() {
+ new AdobePathWriter(new Path2D.Double(Path2D.WIND_NON_ZERO));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateWriterOutOfBounds() {
+ Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
+ path.append(new Ellipse2D.Double(.5, 0.5, 2, 2), false);
+
+ new AdobePathWriter(path);
+ }
+
+ @Test
+ public void testCreateWriterValid() {
+ Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
+ path.append(new Ellipse2D.Double(.25, .25, .5, .5), false);
+
+ new AdobePathWriter(path);
+ }
+
+ @Test
+ public void testCreateWriterMulti() {
+ Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.append(new Ellipse2D.Float(.25f, .25f, .5f, .5f), false);
+ path.append(new Rectangle2D.Double(0, 0, 1, .5), false);
+ path.append(new Polygon(new int[] {1, 2, 0, 1}, new int[] {0, 2, 2, 0}, 4)
+ .getPathIterator(AffineTransform.getScaleInstance(1 / 2.0, 1 / 2.0)), false);
+
+ new AdobePathWriter(path);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateNotClosed() {
+ GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.moveTo(.5, .5);
+ path.lineTo(1, .5);
+ path.curveTo(1, 1, 1, 1, .5, 1);
+
+ new AdobePathWriter(path).writePath();
+ }
+
+ @Test
+ public void testCreateClosed() {
+ GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.moveTo(.5, .5);
+ path.lineTo(1, .5);
+ path.curveTo(1, 1, 1, 1, .5, 1);
+ path.closePath();
+
+ byte[] bytes = new AdobePathWriter(path).writePath();
+
+ assertEquals(6 * 26, bytes.length);
+
+ int off = 0;
+
+ // Path/initial fill rule: Even-Odd (0)
+ assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Rectangle 1: 0, 0, 1, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Sanity
+ assertEquals(bytes.length, off);
+ }
+
+ @Test
+ public void testCreateImplicitClosed() {
+ GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.moveTo(.5, .5);
+ path.lineTo(1, .5);
+ path.curveTo(1, 1, 1, 1, .5, 1);
+ path.lineTo(.5, .5);
+
+ byte[] bytes = new AdobePathWriter(path).writePath();
+
+ assertEquals(6 * 26, bytes.length);
+
+ int off = 0;
+
+ // Path/initial fill rule: Even-Odd (0)
+ assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Rectangle 1: 0, 0, 1, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Sanity
+ assertEquals(bytes.length, off);
+
+ }
+
+ @Test
+ public void testCreateDoubleClosed() {
+ GeneralPath path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.moveTo(.5, .5);
+ path.lineTo(1, .5);
+ path.curveTo(1, 1, 1, 1, .5, 1);
+ path.lineTo(.5, .5);
+ path.closePath();
+
+ byte[] bytes = new AdobePathWriter(path).writePath();
+
+ assertEquals(6 * 26, bytes.length);
+
+ int off = 0;
+
+ // Path/initial fill rule: Even-Odd (0)
+ assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Rectangle 1: 0, 0, 1, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0, 0, -128, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Sanity
+ assertEquals(bytes.length, off);
+ }
+
+ @Test
+ public void testWriteToStream() throws IOException {
+ Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.append(new Ellipse2D.Double(0, 0, 1, 1), false);
+ path.append(new Ellipse2D.Double(.5, .5, .5, .5), false);
+ path.append(new Ellipse2D.Double(.25, .25, .5, .5), false);
+
+ AdobePathWriter pathCreator = new AdobePathWriter(path);
+
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ try (ImageOutputStream output = ImageIO.createImageOutputStream(byteStream)) {
+ pathCreator.writePath(output);
+ }
+
+ assertEquals(17 * 26, byteStream.size());
+
+ byte[] bytes = byteStream.toByteArray();
+
+ int off = 0;
+
+ // Path/initial fill rule: Even-Odd (0)
+ assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Elipse 1: 0, 0, 1, 1
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 57, 78, -68, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 0, -58, -79, 68, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 1, 0, 0, 0, 0, -58, -79, 68, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 0, 57, 78, -68},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -58, -79, 68, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 57, 78, -68, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 0, 0, 0, 57, 78, -68, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, -58, -79, 68},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Elipse 2: .5, .5, .5, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -100, -89, 94, 1, 0, 0, 0, 0, -64, 0, 0, 1, 0, 0, 0, 0, -29, 88, -94, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 1, 0, 0, 0, 0, -29, 88, -94, 1, 0, 0, 0, 0, -64, 0, 0, 1, 0, 0, 0, 0, -100, -89, 94},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -29, 88, -94, 0, -128, 0, 0, 0, -64, 0, 0, 0, -128, 0, 0, 0, -100, -89, 94, 0, -128, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -128, 0, 0, 0, -100, -89, 94, 0, -128, 0, 0, 0, -64, 0, 0, 0, -128, 0, 0, 0, -29, 88, -94},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Elipse32: .25, .25, .5, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 92, -89, 94, 0, -64, 0, 0, 0, -128, 0, 0, 0, -64, 0, 0, 0, -93, 88, -94, 0, -64, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -64, 0, 0, 0, -93, 88, -94, 0, -64, 0, 0, 0, -128, 0, 0, 0, -64, 0, 0, 0, 92, -89, 94},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, -93, 88, -94, 0, 64, 0, 0, 0, -128, 0, 0, 0, 64, 0, 0, 0, 92, -89, 94, 0, 64, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_LINKED, 0, 64, 0, 0, 0, 92, -89, 94, 0, 64, 0, 0, 0, -128, 0, 0, 0, 64, 0, 0, 0, -93, 88, -94},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Sanity
+ assertEquals(bytes.length, off);
+ }
+
+ @Test
+ public void testCreateArray() {
+ Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.append(new Rectangle2D.Double(0, 0, 1, .5), false);
+ path.append(new Rectangle2D.Double(.25, .25, .5, .5), false);
+
+ AdobePathWriter pathCreator = new AdobePathWriter(path);
+
+ byte[] bytes = pathCreator.writePath();
+
+ assertEquals(12 * 26, bytes.length);
+
+ int off = 0;
+
+ // Path/initial fill rule: Even-Odd (0)
+ assertArrayEquals(new byte[] {0, PATH_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, INITIAL_FILL_RULE_RECORD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Rectangle 1: 0, 0, 1, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 1, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -128, 0, 0, 0, 0, 0, 0, 0, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Rectangle 2: .25, .25, .5, .5
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_LENGTH_RECORD, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, -64, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, 64, 0, 0, 0, -64, 0, 0, 0, 64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, -64, 0, 0, 0, 64, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+ assertArrayEquals(new byte[] {0, CLOSED_SUBPATH_BEZIER_UNLINKED, 0, -64, 0, 0, 0, 64, 0, 0, 0, -64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0, 0, 64, 0, 0},
+ Arrays.copyOfRange(bytes, off, off += 26));
+
+ // Sanity
+ assertEquals(bytes.length, off);
+ }
+
+ @Test
+ public void testRoundtrip0() throws IOException {
+ Path2D path = new GeneralPath(Path2D.WIND_EVEN_ODD);
+ path.append(new Rectangle2D.Double(0, 0, 1, .5), false);
+ path.append(new Rectangle2D.Double(.25, .25, .5, .5), false);
+
+ byte[] bytes = new AdobePathWriter(path).writePath();
+ Path2D readPath = new AdobePathReader(new ByteArrayImageInputStream(bytes)).readPath();
+
+ assertEquals(path.getWindingRule(), readPath.getWindingRule());
+ assertEquals(path.getBounds2D(), readPath.getBounds2D());
+
+ // TODO: Would be nice, but hard to do, as we convert all points to cubic...
+// assertPathEquals(path, readPath);
+ }
+
+ @Test
+ public void testRoundtrip1() throws IOException {
+ // We'll read this from a real file, with hardcoded offsets for simplicity
+ // PSD IRB: offset: 34, length: 32598
+ // Clipping path: offset: 31146, length: 1248
+ ImageInputStream stream = PathsTest.resourceAsIIOStream("/psd/grape_with_path.psd");
+ stream.seek(34 + 31146);
+ byte[] data = new byte[1248];
+ stream.readFully(data);
+
+ Path2D path = new AdobePathReader(data).readPath();
+ byte[] bytes = new AdobePathWriter(path).writePath();
+
+ Path2D readPath = new AdobePathReader(new ByteArrayImageInputStream(bytes)).readPath();
+ assertEquals(path.getWindingRule(), readPath.getWindingRule());
+ assertEquals(path.getBounds2D(), readPath.getBounds2D());
+
+ assertPathEquals(path, readPath);
+
+ assertEquals(data.length, bytes.length);
+
+ // Path segment 3 contains some unknown bits in the filler bytes, we'll ignore those...
+ cleanLengthRecords(data);
+
+ assertEquals(formatSegments(data), formatSegments(bytes));
+ assertArrayEquals(data, bytes);
+ }
+
+ private static void cleanLengthRecords(byte[] data) {
+ for (int i = 0; i < data.length; i += 26) {
+ if (data[i + 1] == CLOSED_SUBPATH_LENGTH_RECORD) {
+ // Clean everything after record type and length field
+ for (int j = 4; j < 26; j++) {
+ data[i + j] = 0;
+ }
+ }
+ }
+ }
+
+ private static String formatSegments(byte[] data) {
+ StringBuilder builder = new StringBuilder(data.length * 5);
+
+ for (int i = 0; i < data.length; i += 26) {
+ builder.append(Arrays.toString(Arrays.copyOfRange(data, i, i + 26))).append('\n');
+ }
+
+ return builder.toString();
+ }
+
+ @Test
+ public void testRoundtrip2() throws IOException {
+ // We'll read this from a real file, with hardcoded offsets for simplicity
+ // PSD IRB: offset: 16970, length: 11152
+ // Clipping path: offset: 9250, length: 1534
+ ImageInputStream stream = PathsTest.resourceAsIIOStream("/tiff/big-endian-multiple-clips.tif");
+ stream.seek(16970 + 9250);
+ byte[] data = new byte[1534];
+ stream.readFully(data);
+
+ Path2D path = new AdobePathReader(data).readPath();
+ byte[] bytes = new AdobePathWriter(path).writePath();
+
+ Path2D readPath = new AdobePathReader(new ByteArrayImageInputStream(bytes)).readPath();
+ assertEquals(path.getWindingRule(), readPath.getWindingRule());
+ assertEquals(path.getBounds2D(), readPath.getBounds2D());
+
+ assertPathEquals(path, readPath);
+
+ assertEquals(data.length, bytes.length);
+
+ // Path segment 3 and 48 contains some unknown bits in the filler bytes, we'll ignore that:
+ cleanLengthRecords(data);
+
+ assertEquals(formatSegments(data), formatSegments(bytes));
+ assertArrayEquals(data, bytes);
+ }
+}
diff --git a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java
index 7aca08ee..9f5b373b 100644
--- a/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java
+++ b/imageio/imageio-clippath/src/test/java/com/twelvemonkeys/imageio/path/PathsTest.java
@@ -38,15 +38,18 @@ import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.geom.GeneralPath;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
/**
* PathsTest.
@@ -125,12 +128,12 @@ public class PathsTest {
}
@Test(expected = IllegalArgumentException.class)
- public void testApplyClippingPathNullPath() throws IOException {
+ public void testApplyClippingPathNullPath() {
Paths.applyClippingPath(null, new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY));
}
@Test(expected = IllegalArgumentException.class)
- public void testApplyClippingPathNullSource() throws IOException {
+ public void testApplyClippingPathNullSource() {
Paths.applyClippingPath(new GeneralPath(), null);
}
@@ -147,7 +150,7 @@ public class PathsTest {
assertEquals(source.getWidth(), image.getWidth());
assertEquals(source.getHeight(), image.getHeight());
// Transparent
- assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
+ assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency());
// Corners (at least) should be transparent
assertEquals(0, image.getRGB(0, 0));
@@ -161,8 +164,9 @@ public class PathsTest {
// TODO: Mor sophisticated test that tests all pixels outside path...
}
+ @SuppressWarnings("ConstantConditions")
@Test(expected = IllegalArgumentException.class)
- public void testApplyClippingPathNullDestination() throws IOException {
+ public void testApplyClippingPathNullDestination() {
Paths.applyClippingPath(new GeneralPath(), new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), null);
}
@@ -209,7 +213,7 @@ public class PathsTest {
assertEquals(857, image.getWidth());
assertEquals(1800, image.getHeight());
// Transparent
- assertTrue(image.getColorModel().getTransparency() == Transparency.TRANSLUCENT);
+ assertEquals(Transparency.TRANSLUCENT, image.getColorModel().getTransparency());
// Corners (at least) should be transparent
assertEquals(0, image.getRGB(0, 0));
@@ -230,34 +234,70 @@ public class PathsTest {
}
static Path2D readExpectedPath(final String resource) throws IOException {
- ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource));
-
- try {
+ try (ObjectInputStream ois = new ObjectInputStream(PathsTest.class.getResourceAsStream(resource))) {
return (Path2D) ois.readObject();
}
catch (ClassNotFoundException e) {
throw new IOException(e);
}
- finally {
- ois.close();
- }
}
static void assertPathEquals(final Path2D expectedPath, final Path2D actualPath) {
+ assertNotNull("Expected path is null, check your tests...", expectedPath);
+ assertNotNull(actualPath);
+
PathIterator expectedIterator = expectedPath.getPathIterator(null);
PathIterator actualIterator = actualPath.getPathIterator(null);
+
float[] expectedCoords = new float[6];
float[] actualCoords = new float[6];
- while(!actualIterator.isDone()) {
+ while(!expectedIterator.isDone()) {
+ assertFalse("Less points than expected", actualIterator.isDone());
+
int expectedType = expectedIterator.currentSegment(expectedCoords);
int actualType = actualIterator.currentSegment(actualCoords);
- assertEquals(expectedType, actualType);
- assertArrayEquals(expectedCoords, actualCoords, 0);
+ assertEquals("Unexpected segment type", expectedType, actualType);
+ assertArrayEquals("Unexpected coordinates", expectedCoords, actualCoords, 0);
actualIterator.next();
expectedIterator.next();
}
+
+ assertTrue("More points than expected", actualIterator.isDone());
+ }
+
+ @Test
+ public void testWriteJPEG() throws IOException {
+ Path2D originalPath = readExpectedPath("/ser/multiple-clips.ser");
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_3BYTE_BGR);
+ try (ImageOutputStream stream = ImageIO.createImageOutputStream(bytes)) {
+ boolean written = Paths.writeClipped(image, originalPath, "JPEG", stream);
+ assertTrue(written);
+ }
+ assertTrue(bytes.size() > 1024); // Actual size may be plugin specific...
+
+ Path2D actualPath = Paths.readPath(new ByteArrayImageInputStream(bytes.toByteArray()));
+ assertPathEquals(originalPath, actualPath);
+ }
+
+ @Test
+ public void testWriteTIFF() throws IOException {
+ Path2D originalPath = readExpectedPath("/ser/grape-path.ser");
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
+ try (ImageOutputStream stream = ImageIO.createImageOutputStream(bytes)) {
+ boolean written = Paths.writeClipped(image, originalPath, "TIFF", stream);
+ assumeTrue(written); // TIFF support is optional
+ }
+
+ assertTrue(bytes.size() > 1024); // Actual size may be plugin specific...
+
+ Path2D actualPath = Paths.readPath(new ByteArrayImageInputStream(bytes.toByteArray()));
+ assertPathEquals(originalPath, actualPath);
}
}
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java
index b20ace02..2c8f25db 100755
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/psd/PSDReader.java
@@ -83,7 +83,6 @@ public final class PSDReader extends MetadataReader {
PSDResource resource = new PSDResource(id, input);
entries.add(new PSDEntry(id, resource.name(), resource.data()));
-
}
catch (EOFException e) {
break;
diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java
index dd85dc88..19171d88 100644
--- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java
+++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java
@@ -144,6 +144,7 @@ public interface TIFF {
int TAG_ROWS_PER_STRIP = 278;
int TAG_STRIP_BYTE_COUNTS = 279;
int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange."
+ int TAG_FREE_BYTE_COUNTS = 289;
// "Old-style" JPEG (still used as EXIF thumbnail)
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
index 594db004..e7199b8b 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java
@@ -142,13 +142,17 @@ public final class TIFFImageWriter extends ImageWriterBase {
private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
throws IOException {
RenderedImage renderedImage = image.getRenderedImage();
-
- TIFFImageMetadata metadata = image.getMetadata() != null
- ? convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param)
- : getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
-
- ColorModel colorModel = renderedImage.getColorModel();
SampleModel sampleModel = renderedImage.getSampleModel();
+
+ // Can't use createFromRenderedImage in this case, as it does not consider palette for TYPE_BYTE_BINARY...
+ // TODO: Consider writing workaround in ImageTypeSpecifiers
+ ImageTypeSpecifier spec = new ImageTypeSpecifier(renderedImage);
+
+ // TODO: Handle case where convertImageMetadata returns null, due to unknown metadata format, or reconsider if that's a valid case...
+ TIFFImageMetadata metadata = image.getMetadata() != null
+ ? convertImageMetadata(image.getMetadata(), spec, param)
+ : getDefaultImageMetadata(spec, param);
+
int numBands = sampleModel.getNumBands();
int pixelSize = computePixelSize(sampleModel);
@@ -170,145 +174,29 @@ public final class TIFFImageWriter extends ImageWriterBase {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
- // TODO: There shouldn't be necessary to create a separate map here, this should be handled in the
- // convertImageMetadata/getDefaultImageMetadata methods....
Map entries = new LinkedHashMap<>();
+ // Copy metadata to output
+ Directory metadataIFD = metadata.getIFD();
+ for (Entry entry : metadataIFD) {
+ entries.put((Integer) entry.getIdentifier(), entry);
+ }
+
entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
- entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
- entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
-
- // If numComponents > numColorComponents, write ExtraSamples
- if (numBands > colorModel.getNumColorComponents()) {
- // TODO: Write per component > numColorComponents
- if (colorModel.hasAlpha()) {
- entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
- }
- else {
- entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
- }
- }
-
- // Write compression field from param or metadata
- int compression;
- if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
- && image.getMetadata() != null && metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION) != null) {
- compression = ((Number) metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION).getValue()).intValue();
- }
- else {
- compression = TIFFImageWriteParam.getCompressionType(param);
- }
-
- entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
-
- // TODO: Let param/metadata control predictor
- // TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
- switch (compression) {
- case TIFFExtension.COMPRESSION_ZLIB:
- case TIFFExtension.COMPRESSION_DEFLATE:
- case TIFFExtension.COMPRESSION_LZW:
- if (pixelSize >= 8) {
- entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
- }
-
- break;
-
- case TIFFExtension.COMPRESSION_CCITT_T4:
- Entry group3options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP3OPTIONS);
-
- if (group3options == null) {
- group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING);
- }
-
- entries.put(TIFF.TAG_GROUP3OPTIONS, group3options);
-
- break;
-
- case TIFFExtension.COMPRESSION_CCITT_T6:
- Entry group4options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP4OPTIONS);
-
- if (group4options == null) {
- group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L);
- }
-
- entries.put(TIFF.TAG_GROUP4OPTIONS, group4options);
-
- break;
-
- default:
- }
-
- int photometric = getPhotometricInterpretation(colorModel, compression);
- entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
-
- if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
- // TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
- // We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
- entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
- entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
- }
- else {
- entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands));
-
- // Note: Assuming sRGB to be the default RGB interpretation
- ColorSpace colorSpace = colorModel.getColorSpace();
- if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
- entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
- }
- }
-
- // Default sample format SAMPLEFORMAT_UINT need not be written
- if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
- entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
- }
- // TODO: Float values!
-
- // TODO: Again, this should be handled in the metadata conversion....
- // Get Software from metadata, or use default
- Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE);
- entries.put(TIFF.TAG_SOFTWARE, software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
-
- // Copy metadata to output
- int[] copyTags = {
- TIFF.TAG_ORIENTATION,
- TIFF.TAG_DATE_TIME,
- TIFF.TAG_DOCUMENT_NAME,
- TIFF.TAG_IMAGE_DESCRIPTION,
- TIFF.TAG_MAKE,
- TIFF.TAG_MODEL,
- TIFF.TAG_PAGE_NAME,
- TIFF.TAG_PAGE_NUMBER,
- TIFF.TAG_ARTIST,
- TIFF.TAG_HOST_COMPUTER,
- TIFF.TAG_COPYRIGHT
- };
- for (int tagID : copyTags) {
- Entry entry = metadata.getIFD().getEntryById(tagID);
- if (entry != null) {
- entries.put(tagID, entry);
- }
- }
-
- // Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults
- // TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent.
- Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION);
- entries.put(TIFF.TAG_X_RESOLUTION, xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
- Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION);
- entries.put(TIFF.TAG_Y_RESOLUTION, yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
- Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT);
- entries.put(TIFF.TAG_RESOLUTION_UNIT, resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
// TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, renderedImage.getHeight()));
- // - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?)
+ // StripByteCounts - for no compression, entire image data...
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1)); // Updated later
- // - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???)
+ // StripOffsets - can be offset to single strip only
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1)); // Updated later
// TODO: If tiled, write tile indexes etc
// Depending on param.getTilingMode
long nextIFDPointerOffset = -1;
+ int compression = ((Number) entries.get(TIFF.TAG_COMPRESSION).getValue()).intValue();
+
if (compression == TIFFBaseline.COMPRESSION_NONE) {
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
long streamPosition = imageOutput.getStreamPosition();
@@ -876,12 +764,58 @@ public final class TIFFImageWriter extends ImageWriterBase {
Map entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20);
+ // Set software as default, may be overwritten
+ entries.put(TIFF.TAG_SOFTWARE, new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
+ entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
+
if (ifd != null) {
for (Entry entry : ifd) {
- entries.put((Integer) entry.getIdentifier(), entry);
+ int tagId = (Integer) entry.getIdentifier();
+
+ switch (tagId) {
+ // Baseline
+ case TIFF.TAG_SUBFILE_TYPE:
+ case TIFF.TAG_OLD_SUBFILE_TYPE:
+ case TIFF.TAG_IMAGE_DESCRIPTION:
+ case TIFF.TAG_MAKE:
+ case TIFF.TAG_MODEL:
+ case TIFF.TAG_ORIENTATION:
+ case TIFF.TAG_X_RESOLUTION:
+ case TIFF.TAG_Y_RESOLUTION:
+ case TIFF.TAG_RESOLUTION_UNIT:
+ case TIFF.TAG_SOFTWARE:
+ case TIFF.TAG_DATE_TIME:
+ case TIFF.TAG_ARTIST:
+ case TIFF.TAG_HOST_COMPUTER:
+ case TIFF.TAG_COPYRIGHT:
+ // Extension
+ case TIFF.TAG_DOCUMENT_NAME:
+ case TIFF.TAG_PAGE_NAME:
+ case TIFF.TAG_X_POSITION:
+ case TIFF.TAG_Y_POSITION:
+ case TIFF.TAG_PAGE_NUMBER:
+ case TIFF.TAG_XMP:
+ // Private/Custom
+ case TIFF.TAG_IPTC:
+ case TIFF.TAG_PHOTOSHOP:
+ case TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA:
+ case TIFF.TAG_PHOTOSHOP_ANNOTATIONS:
+ case TIFF.TAG_EXIF_IFD:
+ case TIFF.TAG_GPS_IFD:
+ case TIFF.TAG_INTEROP_IFD:
+ entries.put(tagId, entry);
+ }
}
}
+ ColorModel colorModel = imageType.getColorModel();
+ SampleModel sampleModel = imageType.getSampleModel();
+ int numBands = sampleModel.getNumBands();
+ int pixelSize = computePixelSize(sampleModel);
+
+ entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
+
+ // Compression field from param or metadata
int compression;
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
&& ifd != null && ifd.getEntryById(TIFF.TAG_COMPRESSION) != null) {
@@ -890,11 +824,81 @@ public final class TIFFImageWriter extends ImageWriterBase {
else {
compression = TIFFImageWriteParam.getCompressionType(param);
}
+ entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
- int photometricInterpretation = getPhotometricInterpretation(imageType.getColorModel(), compression);
+ // TODO: Allow metadata to take precedence?
+ int photometricInterpretation = getPhotometricInterpretation(colorModel, compression);
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, photometricInterpretation));
- // TODO: Set values from param if != null + combined values...
+ // If numComponents > numColorComponents, write ExtraSamples
+ if (numBands > colorModel.getNumColorComponents()) {
+ // TODO: Write per component > numColorComponents
+ if (colorModel.hasAlpha()) {
+ entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
+ }
+ else {
+ entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
+ }
+ }
+
+ switch (compression) {
+ case TIFFExtension.COMPRESSION_ZLIB:
+ case TIFFExtension.COMPRESSION_DEFLATE:
+ case TIFFExtension.COMPRESSION_LZW:
+ // TODO: Let param/metadata control predictor
+ // TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
+ if (pixelSize >= 8) {
+ entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
+ }
+
+ break;
+
+ case TIFFExtension.COMPRESSION_CCITT_T4:
+ Entry group3options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP3OPTIONS) : null;
+
+ if (group3options == null) {
+ group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING);
+ }
+
+ entries.put(TIFF.TAG_GROUP3OPTIONS, group3options);
+
+ break;
+
+ case TIFFExtension.COMPRESSION_CCITT_T6:
+ Entry group4options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP4OPTIONS) : null;
+
+ if (group4options == null) {
+ group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L);
+ }
+
+ entries.put(TIFF.TAG_GROUP4OPTIONS, group4options);
+
+ break;
+
+ default:
+ }
+
+ if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
+ // TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
+ // We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
+ entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
+ entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
+ }
+ else {
+ entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands));
+
+ // Note: Assuming sRGB to be the default RGB interpretation
+ ColorSpace colorSpace = colorModel.getColorSpace();
+ if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
+ entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
+ }
+ }
+
+ // Default sample format SAMPLEFORMAT_UINT need not be written
+ if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
+ entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
+ }
+ // TODO: Float values!
return new TIFFImageMetadata(entries.values());
}
diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
index 164ad217..9f971437 100644
--- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
+++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java
@@ -616,7 +616,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
int maxH = Math.min(300, image.getHeight());
for (int y = 0; y < maxH; y++) {
for (int x = 0; x < image.getWidth(); x++) {
- assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0);
+ assertRGBEquals(String.format("Pixel differ: @%d,%d", x, y), orig.getRGB(x, y), image.getRGB(x, y), 0);
}
}
@@ -654,7 +654,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
assumeNotNull(original);
- // Write it back, using same compression (copied from metadata)
+ // Write it back, using deflate compression
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
@@ -718,7 +718,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
assumeNotNull(original);
- // Write it back, using same compression (copied from metadata)
+ // Write it back, no compression
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {