mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 20:15:28 -04:00
#490: Now allows writing paths in TIFF and JPEG.
This commit is contained in:
parent
768bc30653
commit
278ce6ef33
@ -67,16 +67,14 @@ public final class AdobePathWriter {
|
|||||||
* regardless of image dimensions.
|
* regardless of image dimensions.
|
||||||
* </p>
|
* </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].
|
* 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},
|
* @throws IllegalArgumentException if {@code path} is {@code null},
|
||||||
* the paths winding rule is not @link Path2D#WIND_EVEN_ODD} or
|
* 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].
|
* 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");
|
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");
|
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));
|
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>...
|
// 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) {
|
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];
|
double[] coords = new double[6];
|
||||||
AdobePathSegment prev = new AdobePathSegment(CLOSED_SUBPATH_BEZIER_LINKED, 0, 0, 0, 0, 0, 0);
|
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) {
|
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) ||
|
return (x1 == x2 && x2 == x3 && y1 == y2 && y2 == y3) ||
|
||||||
(x1 != x2 || y1 != y2) && (x2 != x3 || 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...
|
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 {
|
void writePathResource(final DataOutput output) throws IOException {
|
||||||
output.writeInt(PSD.RESOURCE_TYPE);
|
output.writeInt(PSD.RESOURCE_TYPE);
|
||||||
output.writeShort(PSD.RES_CLIPPING_PATH);
|
output.writeShort(PSD.RES_CLIPPING_PATH);
|
||||||
@ -170,6 +177,12 @@ public final class AdobePathWriter {
|
|||||||
writePath(output);
|
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 {
|
public void writePath(final DataOutput output) throws IOException {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
System.out.println("segments: " + segments.size());
|
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?
|
// 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() {
|
public byte[] writePath() {
|
||||||
// TODO: Do we need to care about endianness for TIFF files?
|
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
try (DataOutputStream stream = new DataOutputStream(bytes)) {
|
try (DataOutputStream stream = new DataOutputStream(bytes)) {
|
||||||
writePath(stream);
|
writePath(stream);
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
throw new AssertionError("Should never.. uh.. Oh well. It happened.", e);
|
throw new AssertionError("ByteArrayOutputStream threw IOException", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.toByteArray();
|
return bytes.toByteArray();
|
||||||
|
@ -43,8 +43,11 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
|||||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
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.ImageInputStream;
|
||||||
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
import javax.imageio.stream.MemoryCacheImageInputStream;
|
import javax.imageio.stream.MemoryCacheImageInputStream;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
@ -52,13 +55,15 @@ import java.awt.geom.Path2D;
|
|||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedHashMap;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static com.twelvemonkeys.lang.Validate.isTrue;
|
import static com.twelvemonkeys.lang.Validate.isTrue;
|
||||||
import static com.twelvemonkeys.lang.Validate.notNull;
|
import static com.twelvemonkeys.lang.Validate.notNull;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
|
import static java.util.Collections.singletonMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Support for various Adobe Photoshop Path related operations:
|
* 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) {
|
else if (magic >>> 16 == JPEG.SOI && (magic & 0xff00) == 0xff00) {
|
||||||
// JPEG version
|
// JPEG version
|
||||||
Map<Integer, java.util.List<String>> segmentIdentifiers = new LinkedHashMap<>();
|
Map<Integer, List<String>> segmentIdentifiers = singletonMap(JPEG.APP13, singletonList("Photoshop 3.0"));
|
||||||
segmentIdentifiers.put(JPEG.APP13, singletonList("Photoshop 3.0"));
|
|
||||||
|
|
||||||
List<JPEGSegment> photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
|
List<JPEGSegment> photoshop = JPEGSegmentUtil.readSegments(stream, segmentIdentifiers);
|
||||||
|
|
||||||
if (!photoshop.isEmpty()) {
|
if (!photoshop.isEmpty()) {
|
||||||
@ -254,6 +257,103 @@ public final class Paths {
|
|||||||
return applyClippingPath(clip, image);
|
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
|
// Test code
|
||||||
public static void main(final String[] args) throws IOException, InterruptedException {
|
public static void main(final String[] args) throws IOException, InterruptedException {
|
||||||
BufferedImage destination;
|
BufferedImage destination;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user