#490: Now allows writing paths in TIFF and JPEG.

This commit is contained in:
Harald Kuhr 2020-01-16 19:31:54 +01:00
parent 768bc30653
commit 278ce6ef33
2 changed files with 138 additions and 12 deletions

View File

@ -67,16 +67,14 @@ public final class AdobePathWriter {
* regardless of image dimensions.
* </p>
*
* @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<List<AdobePathSegment>...
private static List<AdobePathSegment> 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: Better name?
public byte[] writePath() {
// 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() {
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();

View File

@ -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<Integer, java.util.List<String>> segmentIdentifiers = new LinkedHashMap<>();
segmentIdentifiers.put(JPEG.APP13, singletonList("Photoshop 3.0"));
Map<Integer, List<String>> segmentIdentifiers = singletonMap(JPEG.APP13, singletonList("Photoshop 3.0"));
List<JPEGSegment> 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<ImageWriter> 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;