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;