From 278ce6ef33a7e113917afb9d0a29346946dfda66 Mon Sep 17 00:00:00 2001
From: Harald Kuhr
Date: Thu, 16 Jan 2020 19:31:54 +0100
Subject: [PATCH] #490: Now allows writing paths in TIFF and JPEG.
---
.../imageio/path/AdobePathWriter.java | 40 +++++--
.../com/twelvemonkeys/imageio/path/Paths.java | 110 +++++++++++++++++-
2 files changed, 138 insertions(+), 12 deletions(-)
diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java
index d387d44f..921fe418 100755
--- a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java
+++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/AdobePathWriter.java
@@ -67,16 +67,14 @@ public final class AdobePathWriter {
* regardless of image dimensions.
*
*
- * @param path A {@code Path2D} instance that has {@link Path2D#WIND_EVEN_ODD WIND_EVEN_ODD} rule
+ * @param path A {@code Shape} instance that has {@link Path2D#WIND_EVEN_ODD WIND_EVEN_ODD} rule
* and is contained within the rectangle [x=0.0,y=0.0,w=1.0,h=1.0].
* @throws IllegalArgumentException if {@code path} is {@code null},
* the paths winding rule is not @link Path2D#WIND_EVEN_ODD} or
* the paths bounding box is outside [x=0.0,y=0.0,w=1.0,h=1.0].
*/
- public AdobePathWriter(final Path2D path) {
+ public AdobePathWriter(final Shape path) {
notNull(path, "path");
- // TODO: Test if PS really ignores winding rule as documented... Otherwise we could support writing non-zero too.
- isTrue(path.getWindingRule() == Path2D.WIND_EVEN_ODD, path.getWindingRule(), "Only even/odd winding rule supported: %d");
isTrue(new Rectangle(0, 0, 1, 1).contains(path.getBounds2D()), path.getBounds2D(), "Path bounds must be within [x=0,y=0,w=1,h=1]: %s");
segments = pathToSegments(path.getPathIterator(null));
@@ -84,6 +82,9 @@ public final class AdobePathWriter {
// TODO: Look at the API so that conversion both ways are aligned. The read part builds a path from List...
private static List pathToSegments(final PathIterator pathIterator) {
+ // TODO: Test if PS really ignores winding rule as documented... Otherwise we could support writing non-zero too.
+ isTrue(pathIterator.getWindingRule() == Path2D.WIND_EVEN_ODD, pathIterator.getWindingRule(), "Only even/odd winding rule supported: %d");
+
double[] coords = new double[6];
AdobePathSegment prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 0, 0, 0, 0);
@@ -154,13 +155,19 @@ public final class AdobePathWriter {
}
private static boolean isCollinear(double x1, double y1, double x2, double y2, double x3, double y3) {
- // PS seems to write as linked if all points are the same....
+ // Photoshop seems to write as linked if all points are the same....
return (x1 == x2 && x2 == x3 && y1 == y2 && y2 == y3) ||
(x1 != x2 || y1 != y2) && (x2 != x3 || y2 != y3) &&
Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) <= COLLINEARITY_THRESHOLD; // With some slack...
}
+ /**
+ * Writes the path as a complete Photoshop clipping path resource to the given stream.
+ *
+ * @param output the stream to write to.
+ * @throws IOException if an I/O exception happens during writing.
+ */
void writePathResource(final DataOutput output) throws IOException {
output.writeInt(PSD.RESOURCE_TYPE);
output.writeShort(PSD.RES_CLIPPING_PATH);
@@ -170,6 +177,12 @@ public final class AdobePathWriter {
writePath(output);
}
+ /**
+ * Writes the path as a set of Adobe path segments to the given stream.
+ *
+ * @param output the stream to write to.
+ * @throws IOException if an I/O exception happens during writing.
+ */
public void writePath(final DataOutput output) throws IOException {
if (DEBUG) {
System.out.println("segments: " + segments.size());
@@ -204,16 +217,29 @@ public final class AdobePathWriter {
}
}
+ // TODO: Do we need to care about endianness for TIFF files?
// TODO: Better name?
+ byte[] writePathResource() {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try (DataOutputStream stream = new DataOutputStream(bytes)) {
+ writePathResource(stream);
+ }
+ catch (IOException e) {
+ throw new AssertionError("ByteArrayOutputStream threw IOException", e);
+ }
+
+ return bytes.toByteArray();
+ }
+
public byte[] writePath() {
- // TODO: Do we need to care about endianness for TIFF files?
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream stream = new DataOutputStream(bytes)) {
writePath(stream);
}
catch (IOException e) {
- throw new AssertionError("Should never.. uh.. Oh well. It happened.", e);
+ throw new AssertionError("ByteArrayOutputStream threw IOException", e);
}
return bytes.toByteArray();
diff --git a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
index 7cf3e33f..79efcbed 100755
--- a/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
+++ b/imageio/imageio-clippath/src/main/java/com/twelvemonkeys/imageio/path/Paths.java
@@ -43,8 +43,11 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
-import javax.imageio.ImageIO;
+import javax.imageio.*;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.*;
import java.awt.geom.AffineTransform;
@@ -52,13 +55,15 @@ import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
-import java.util.LinkedHashMap;
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
/**
* Support for various Adobe Photoshop Path related operations:
@@ -119,9 +124,7 @@ public final class Paths {
}
else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) {
// JPEG version
- Map> segmentIdentifiers = new LinkedHashMap<>();
- segmentIdentifiers.put(JPEG.APP13, singletonList("Photoshop 3.0"));
-
+ Map> segmentIdentifiers = singletonMap(JPEG.APP13, singletonList("Photoshop 3.0"));
List photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
if (!photoshop.isEmpty()) {
@@ -254,6 +257,103 @@ public final class Paths {
return applyClippingPath(clip, image);
}
+ public static boolean writeClipped(final BufferedImage 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!");
+ }
+
+ String format = "JPG".equalsIgnoreCase(formatName) ? "JPEG" : formatName.toUpperCase();
+
+ if ("TIFF".equals(format) || "JPEG".equals(format)) {
+ 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);
+
+ byte[] pathResource = new AdobePathWriter(clipPath).writePathResource();
+
+ if ("TIFF".equals(format)) {
+ param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+ param.setCompressionType("Deflate");
+
+ String metadataFormat = "com_sun_media_imageio_plugins_tiff_image_1.0";
+ 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);
+ }
+ else if ("JPEG".equals(format)) {
+ 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);
+ }
+ // TODO: Else if PSD... Requires PSD write + new metadata format...
+
+ writer.setOutput(output);
+ writer.write(null, new IIOImage(image, null, metadata), param);
+
+ return true;
+ }
+ }
+
+ 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(", ");
+ }
+ }
+
// Test code
public static void main(final String[] args) throws IOException, InterruptedException {
BufferedImage destination;