TMI-139: Support for writing TIFF files with custom resolution value.

This commit is contained in:
Harald Kuhr
2015-08-12 10:48:58 +02:00
parent 517fc770bd
commit c913ef445b
7 changed files with 1600 additions and 103 deletions
@@ -1,18 +1,24 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.AbstractMetadata;
import com.twelvemonkeys.imageio.metadata.AbstractDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.lang.Validate;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.lang.reflect.Array;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.*;
/**
* TIFFImageMetadata.
@@ -23,16 +29,19 @@ import java.util.Calendar;
*/
final class TIFFImageMetadata extends AbstractMetadata {
private final Directory ifd;
static final int RATIONAL_SCALE_FACTOR = 100000;
private final Directory original;
private Directory ifd;
TIFFImageMetadata(final Directory ifd) {
super(true, TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, TIFFMedataFormat.class.getName(), null, null);
this.ifd = Validate.notNull(ifd, "IFD");
this.original = ifd;
}
@Override
public boolean isReadOnly() {
return false;
TIFFImageMetadata(final Collection<Entry> entries) {
this(new TIFFIFD(entries));
}
protected IIOMetadataNode getNativeTree() {
@@ -99,7 +108,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
IIOMetadataNode elementNode = new IIOMetadataNode(typeName);
valueNode.appendChild(elementNode);
setValue(value, unsigned, elementNode);
setTIFFNativeValue(value, unsigned, elementNode);
}
else {
for (int i = 0; i < count; i++) {
@@ -107,7 +116,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
IIOMetadataNode elementNode = new IIOMetadataNode(typeName);
valueNode.appendChild(elementNode);
setValue(val, unsigned, elementNode);
setTIFFNativeValue(val, unsigned, elementNode);
}
}
}
@@ -119,7 +128,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
return ifdNode;
}
private void setValue(final Object value, final boolean unsigned, final IIOMetadataNode elementNode) {
private void setTIFFNativeValue(final Object value, final boolean unsigned, final IIOMetadataNode elementNode) {
if (unsigned && value instanceof Byte) {
elementNode.setAttribute("value", String.valueOf((Byte) value & 0xFF));
}
@@ -289,12 +298,12 @@ final class TIFFImageMetadata extends AbstractMetadata {
// Handle ColorSpaceType (RGB/CMYK/YCbCr etc)...
Entry photometricTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
int photometricValue = ((Number) photometricTag.getValue()).intValue(); // No default for this tag!
int photometricValue = getValueAsInt(photometricTag); // No default for this tag!
Entry samplesPerPixelTag = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
Entry bitsPerSampleTag = ifd.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
int numChannelsValue = samplesPerPixelTag != null
? ((Number) samplesPerPixelTag.getValue()).intValue()
? getValueAsInt(samplesPerPixelTag)
: bitsPerSampleTag.valueCount();
IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType");
@@ -393,7 +402,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
Entry compressionTag = ifd.getEntryById(TIFF.TAG_COMPRESSION);
int compressionValue = compressionTag == null
? TIFFBaseline.COMPRESSION_NONE
: ((Number) compressionTag.getValue()).intValue();
: getValueAsInt(compressionTag);
// Naming is identical to JAI ImageIO metadata as far as possible
switch (compressionValue) {
@@ -502,7 +511,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
Entry planarConfigurationTag = ifd.getEntryById(TIFF.TAG_PLANAR_CONFIGURATION);
int planarConfigurationValue = planarConfigurationTag == null
? TIFFBaseline.PLANARCONFIG_CHUNKY
: ((Number) planarConfigurationTag.getValue()).intValue();
: getValueAsInt(planarConfigurationTag);
switch (planarConfigurationValue) {
case TIFFBaseline.PLANARCONFIG_CHUNKY:
@@ -519,14 +528,16 @@ final class TIFFImageMetadata extends AbstractMetadata {
Entry photometricInterpretationTag = ifd.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
int photometricInterpretationValue = photometricInterpretationTag == null
? TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO
: ((Number) photometricInterpretationTag.getValue()).intValue();
: getValueAsInt(photometricInterpretationTag);
Entry samleFormatTag = ifd.getEntryById(TIFF.TAG_SAMPLE_FORMAT);
// TODO: Fix for sampleformat 1 1 1 (as int[]) ??!?!?
int sampleFormatValue = samleFormatTag == null
? TIFFBaseline.SAMPLEFORMAT_UINT
: ((Number) samleFormatTag.getValue()).intValue();
: getValueAsInt(samleFormatTag);
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
node.appendChild(sampleFormat);
switch (sampleFormatValue) {
case TIFFBaseline.SAMPLEFORMAT_UINT:
if (photometricInterpretationValue == TIFFBaseline.PHOTOMETRIC_PALETTE) {
@@ -562,13 +573,13 @@ final class TIFFImageMetadata extends AbstractMetadata {
Entry samplesPerPixelTag = ifd.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL);
int numChannelsValue = samplesPerPixelTag != null
? ((Number) samplesPerPixelTag.getValue()).intValue()
? getValueAsInt(samplesPerPixelTag)
: bitsPerSampleTag.valueCount();
// SampleMSB
Entry fillOrderTag = ifd.getEntryById(TIFF.TAG_FILL_ORDER);
int fillOrder = fillOrderTag != null
? ((Number) fillOrderTag.getValue()).intValue()
? getValueAsInt(fillOrderTag)
: TIFFBaseline.FILL_LEFT_TO_RIGHT;
IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
node.appendChild(sampleMSB);
@@ -588,6 +599,22 @@ final class TIFFImageMetadata extends AbstractMetadata {
return node;
}
private static int getValueAsInt(final Entry entry) {
Object value = entry.getValue();
if (value instanceof Number) {
return ((Number) value).intValue();
}
else if (value instanceof short[]) {
return ((short[]) value)[0];
}
else if (value instanceof int[]) {
return ((int[]) value)[0];
}
throw new IllegalArgumentException("Unsupported type: " + entry);
}
// TODO: Candidate superclass method!
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
@@ -620,7 +647,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
// ImageOrientation
Entry orientationTag = ifd.getEntryById(TIFF.TAG_ORIENTATION);
if (orientationTag != null) {
int orientationValue = ((Number) orientationTag.getValue()).intValue();
int orientationValue = getValueAsInt(orientationTag);
String value = null;
switch (orientationValue) {
@@ -659,7 +686,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
}
Entry resUnitTag = ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT);
int resUnitValue = resUnitTag == null ? TIFFBaseline.RESOLUTION_UNIT_DPI : ((Number) resUnitTag.getValue()).intValue();
int resUnitValue = resUnitTag == null ? TIFFBaseline.RESOLUTION_UNIT_DPI : getValueAsInt(resUnitTag);
if (resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER || resUnitValue == TIFFBaseline.RESOLUTION_UNIT_DPI) {
// 10 mm in 1 cm or 25.4 mm in 1 inch
double scale = resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER ? 10 : 25.4;
@@ -703,7 +730,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
if (extraSamplesTag != null) {
int extraSamplesValue = (extraSamplesTag.getValue() instanceof Number)
? ((Number) extraSamplesTag.getValue()).intValue()
? getValueAsInt(extraSamplesTag)
: ((Number) Array.get(extraSamplesTag.getValue(), 0)).intValue();
// Other values exists, these are not alpha
@@ -739,7 +766,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
if (subFileTypeTag != null) {
// NOTE: The JAI metadata is somewhat broken here, as these are bit flags, not values...
String value = null;
int subFileTypeValue = ((Number) subFileTypeTag.getValue()).intValue();
int subFileTypeValue = getValueAsInt(subFileTypeTag);
if ((subFileTypeValue & TIFFBaseline.FILETYPE_MASK) != 0) {
value = "TransparencyMask";
}
@@ -795,6 +822,7 @@ final class TIFFImageMetadata extends AbstractMetadata {
addTextEntryIfPresent(text, TIFF.TAG_IMAGE_DESCRIPTION);
addTextEntryIfPresent(text, TIFF.TAG_MAKE);
addTextEntryIfPresent(text, TIFF.TAG_MODEL);
addTextEntryIfPresent(text, TIFF.TAG_PAGE_NAME);
addTextEntryIfPresent(text, TIFF.TAG_SOFTWARE);
addTextEntryIfPresent(text, TIFF.TAG_ARTIST);
addTextEntryIfPresent(text, TIFF.TAG_HOST_COMPUTER);
@@ -821,4 +849,435 @@ final class TIFFImageMetadata extends AbstractMetadata {
// See http://stackoverflow.com/questions/30910719/javax-imageio-1-0-standard-plug-in-neutral-metadata-format-tiling-information
return super.getStandardTileNode();
}
/// Mutation
@Override
public boolean isReadOnly() {
return false;
}
public void setFromTree(final String formatName, final Node root) throws IIOInvalidTreeException {
// Standard validation
super.mergeTree(formatName, root);
// Set by "merging" with empty map
LinkedHashMap<Integer, Entry> entries = new LinkedHashMap<>();
mergeEntries(formatName, root, entries);
// TODO: Consistency validation?
// Finally create a new IFD from merged values
ifd = new TIFFIFD(entries.values());
}
@Override
public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException {
// Standard validation
super.mergeTree(formatName, root);
// Clone entries (shallow clone, as entries themselves are immutable)
LinkedHashMap<Integer, Entry> entries = new LinkedHashMap<>(ifd.size() + 10);
for (Entry entry : ifd) {
entries.put((Integer) entry.getIdentifier(), entry);
}
mergeEntries(formatName, root, entries);
// TODO: Consistency validation?
// Finally create a new IFD from merged values
ifd = new TIFFIFD(entries.values());
}
private void mergeEntries(final String formatName, final Node root, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
// Merge from both native and standard trees
if (getNativeMetadataFormatName().equals(formatName)) {
mergeNativeTree(root, entries);
}
else if (IIOMetadataFormatImpl.standardMetadataFormatName.equals(formatName)) {
mergeStandardTree(root, entries);
}
else {
// Should already be checked for
throw new AssertionError();
}
}
private void mergeStandardTree(final Node root, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
NodeList nodes = root.getChildNodes();
// Merge selected values from standard tree
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if ("Dimension".equals(node.getNodeName())) {
mergeFromStandardDimensionNode(node, entries);
}
else if ("Document".equals(node.getNodeName())) {
mergeFromStandardDocumentNode(node, entries);
}
else if ("Text".equals(node.getNodeName())) {
mergeFromStandardTextNode(node, entries);
}
}
}
private void mergeFromStandardDimensionNode(final Node dimensionNode, final Map<Integer, Entry> entries) {
// Dimension: xRes/yRes
// - If set, set res unit to pixels per cm as this better reflects values?
// - Or, convert to DPI, if we already had values in DPI??
// Also, if we have only aspect, set these values, and use unknown as unit?
// TODO: ImageOrientation => Orientation
NodeList children = dimensionNode.getChildNodes();
Float aspect = null;
Float xRes = null;
Float yRes = null;
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
String nodeName = child.getNodeName();
if ("PixelAspectRatio".equals(nodeName)) {
aspect = Float.parseFloat(getAttribute(child, "value"));
}
else if ("HorizontalPixelSize".equals(nodeName)) {
xRes = Float.parseFloat(getAttribute(child, "value"));
}
else if ("VerticalPixelSize".equals(nodeName)) {
yRes = Float.parseFloat(getAttribute(child, "value"));
}
}
// If we have one size compute the other
if (xRes == null && yRes != null) {
xRes = yRes * (aspect != null ? aspect : 1f);
}
else if (yRes == null && xRes != null) {
yRes = xRes / (aspect != null ? aspect : 1f);
}
// If we have resolution
if (xRes != null && yRes != null) {
// If old unit was DPI, convert values and keep DPI, otherwise use PPCM
Entry resUnitEntry = entries.get(TIFF.TAG_RESOLUTION_UNIT);
int resUnitValue = resUnitEntry != null && resUnitEntry.getValue() != null
&& ((Number) resUnitEntry.getValue()).intValue() == TIFFBaseline.RESOLUTION_UNIT_DPI
? TIFFBaseline.RESOLUTION_UNIT_DPI
: TIFFBaseline.RESOLUTION_UNIT_CENTIMETER;
// Units from standard format are pixels per mm, convert to cm or inches
float scale = resUnitValue == TIFFBaseline.RESOLUTION_UNIT_CENTIMETER ? 10 : 25.4f;
int x = Math.round(xRes * scale * RATIONAL_SCALE_FACTOR);
int y = Math.round(yRes * scale * RATIONAL_SCALE_FACTOR);
entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(x, RATIONAL_SCALE_FACTOR)));
entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(y, RATIONAL_SCALE_FACTOR)));
entries.put(TIFF.TAG_RESOLUTION_UNIT,
new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resUnitValue));
}
else if (aspect != null) {
if (aspect >= 1) {
int v = Math.round(aspect * RATIONAL_SCALE_FACTOR);
entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR)));
entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(1)));
}
else {
int v = Math.round(RATIONAL_SCALE_FACTOR / aspect);
entries.put(TIFF.TAG_X_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(1)));
entries.put(TIFF.TAG_Y_RESOLUTION, new TIFFImageWriter.TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(v, RATIONAL_SCALE_FACTOR)));
}
entries.put(TIFF.TAG_RESOLUTION_UNIT,
new TIFFImageWriter.TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_NONE));
}
// Else give up...
}
private void mergeFromStandardDocumentNode(final Node documentNode, final Map<Integer, Entry> entries) {
// Document: SubfileType, CreationDate
NodeList children = documentNode.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
String nodeName = child.getNodeName();
if ("SubimageInterpretation".equals(nodeName)) {
// TODO: SubFileType
}
else if ("ImageCreationTime".equals(nodeName)) {
// TODO: CreationDate
}
}
}
private void mergeFromStandardTextNode(final Node textNode, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
NodeList textEntries = textNode.getChildNodes();
for (int i = 0; i < textEntries.getLength(); i++) {
Node textEntry = textEntries.item(i);
if (!"TextEntry".equals(textEntry.getNodeName())) {
throw new IIOInvalidTreeException("Text node should only contain TextEntry nodes", textNode);
}
String keyword = getAttribute(textEntry, "keyword");
String value = getAttribute(textEntry, "value");
// DocumentName, ImageDescription, Make, Model, PageName,
// Software, Artist, HostComputer, InkNames, Copyright
if (value != null && !value.isEmpty() && keyword != null) {
// We do all comparisons in lower case, for compatibility
keyword = keyword.toLowerCase();
TIFFImageWriter.TIFFEntry entry;
if ("documentname".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_DOCUMENT_NAME, TIFF.TYPE_ASCII, value);
}
else if ("imagedescription".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_IMAGE_DESCRIPTION, TIFF.TYPE_ASCII, value);
}
else if ("make".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MAKE, TIFF.TYPE_ASCII, value);
}
else if ("model".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_MODEL, TIFF.TYPE_ASCII, value);
}
else if ("pagename".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_PAGE_NAME, TIFF.TYPE_ASCII, value);
}
else if ("software".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, value);
}
else if ("artist".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_ARTIST, TIFF.TYPE_ASCII, value);
}
else if ("hostcomputer".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_HOST_COMPUTER, TIFF.TYPE_ASCII, value);
}
else if ("inknames".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_INK_NAMES, TIFF.TYPE_ASCII, value);
}
else if ("copyright".equals(keyword)) {
entry = new TIFFImageWriter.TIFFEntry(TIFF.TAG_COPYRIGHT, TIFF.TYPE_ASCII, value);
}
else {
continue;
}
entries.put((Integer) entry.getIdentifier(), entry);
}
}
}
private void mergeNativeTree(final Node root, final Map<Integer, Entry> entries) throws IIOInvalidTreeException {
Directory ifd = toIFD(root.getFirstChild());
// Merge (overwrite) entries with entries from IFD
for (Entry entry : ifd) {
entries.put((Integer) entry.getIdentifier(), entry);
}
}
private Directory toIFD(final Node ifdNode) throws IIOInvalidTreeException {
if (ifdNode == null || !ifdNode.getNodeName().equals("TIFFIFD")) {
throw new IIOInvalidTreeException("Expected \"TIFFIFD\" node", ifdNode);
}
List<Entry> entries = new ArrayList<>();
NodeList nodes = ifdNode.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
entries.add(toEntry(nodes.item(i)));
}
return new TIFFIFD(entries);
}
private Entry toEntry(final Node node) throws IIOInvalidTreeException {
String name = node.getNodeName();
if (name.equals("TIFFIFD")) {
int tag = Integer.parseInt(getAttribute(node, "parentTagNumber"));
Directory subIFD = toIFD(node);
return new TIFFImageWriter.TIFFEntry(tag, TIFF.TYPE_IFD, subIFD);
}
else if (name.equals("TIFFField")) {
int tag = Integer.parseInt(getAttribute(node, "number"));
short type = getTIFFType(node);
Object value = getValue(node, type);
return value != null ? new TIFFImageWriter.TIFFEntry(tag, type, value) : null;
}
else {
throw new IIOInvalidTreeException("Expected \"TIFFIFD\" or \"TIFFField\" node: " + name, node);
}
}
private short getTIFFType(final Node node) throws IIOInvalidTreeException {
Node containerNode = node.getFirstChild();
if (containerNode == null) {
throw new IIOInvalidTreeException("Missing value wrapper node", node);
}
String nodeName = containerNode.getNodeName();
if (!nodeName.startsWith("TIFF")) {
throw new IIOInvalidTreeException("Unexpected value wrapper node, expected type", containerNode);
}
String typeName = nodeName.substring(4);
if (typeName.equals("Undefined")) {
return TIFF.TYPE_UNDEFINED;
}
typeName = typeName.substring(0, typeName.length() - 1).toUpperCase();
for (int i = 1; i < TIFF.TYPE_NAMES.length; i++) {
if (typeName.equals(TIFF.TYPE_NAMES[i])) {
return (short) i;
}
}
throw new IIOInvalidTreeException("Unknown TIFF type: " + typeName, containerNode);
}
private Object getValue(final Node node, final short type) throws IIOInvalidTreeException {
Node child = node.getFirstChild();
if (child != null) {
String typeName = child.getNodeName();
if (type == TIFF.TYPE_UNDEFINED) {
String values = getAttribute(child, "value");
String[] vals = values.split(",\\s?");
byte[] bytes = new byte[vals.length];
for (int i = 0; i < vals.length; i++) {
bytes[i] = Byte.parseByte(vals[i]);
}
return bytes;
}
else {
NodeList valueNodes = child.getChildNodes();
// Create array for each type
int count = valueNodes.getLength();
Object value = createArrayForType(type, count);
// Parse each value
for (int i = 0; i < count; i++) {
Node valueNode = valueNodes.item(i);
if (!typeName.startsWith(valueNode.getNodeName())) {
throw new IIOInvalidTreeException("Value node does not match container node", child);
}
String stringValue = getAttribute(valueNode, "value");
// NOTE: The reason for parsing "wider" type, is to allow for unsigned values
switch (type) {
case TIFF.TYPE_BYTE:
case TIFF.TYPE_SBYTE:
((byte[]) value)[i] = (byte) Short.parseShort(stringValue);
break;
case TIFF.TYPE_ASCII:
((String[]) value)[i] = stringValue;
break;
case TIFF.TYPE_SHORT:
case TIFF.TYPE_SSHORT:
((short[]) value)[i] = (short) Integer.parseInt(stringValue);
break;
case TIFF.TYPE_LONG:
case TIFF.TYPE_SLONG:
((int[]) value)[i] = (int) Long.parseLong(stringValue);
break;
case TIFF.TYPE_RATIONAL:
case TIFF.TYPE_SRATIONAL:
String[] numDenom = stringValue.split("/");
((Rational[]) value)[i] = numDenom.length > 1
? new Rational(Long.parseLong(numDenom[0]), Long.parseLong(numDenom[1]))
: new Rational(Long.parseLong(numDenom[0]));
break;
case TIFF.TYPE_FLOAT:
((float[]) value)[i] = Float.parseFloat(stringValue);
break;
case TIFF.TYPE_DOUBLE:
((double[]) value)[i] = Double.parseDouble(stringValue);
break;
default:
throw new AssertionError("Unsupported TIFF type: " + type);
}
}
// Normalize value
if (count == 0) {
return null;
}
if (count == 1) {
return Array.get(value, 0);
}
return value;
}
}
throw new IIOInvalidTreeException("Empty TIFField node", node);
}
private Object createArrayForType(final short type, final int length) {
switch (type) {
case TIFF.TYPE_ASCII:
return new String[length];
case TIFF.TYPE_BYTE:
case TIFF.TYPE_SBYTE:
case TIFF.TYPE_UNDEFINED: // Not used here, but for completeness
return new byte[length];
case TIFF.TYPE_SHORT:
case TIFF.TYPE_SSHORT:
return new short[length];
case TIFF.TYPE_LONG:
case TIFF.TYPE_SLONG:
return new int[length];
case TIFF.TYPE_IFD:
return new long[length];
case TIFF.TYPE_RATIONAL:
case TIFF.TYPE_SRATIONAL:
return new Rational[length];
case TIFF.TYPE_FLOAT:
return new float[length];
case TIFF.TYPE_DOUBLE:
return new double[length];
default:
throw new AssertionError("Unsupported TIFF type: " + type);
}
}
private String getAttribute(final Node node, final String attribute) {
return node instanceof Element ? ((Element) node).getAttribute(attribute) : null;
}
@Override
public void reset() {
super.reset();
ifd = original;
}
Directory getIFD() {
return ifd;
}
// TODO: Replace with IFD class when moved to new package and made public!
private final static class TIFFIFD extends AbstractDirectory {
public TIFFIFD(final Collection<Entry> entries) {
super(entries);
}
}
}
@@ -36,7 +36,10 @@ import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
@@ -64,7 +67,9 @@ import java.awt.image.*;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
@@ -82,15 +87,16 @@ import java.util.zip.InflaterInputStream;
* In addition, it supports many common TIFF extensions such as:
* <ul>
* <li>Tiling</li>
* <li>Class F (Facsimile), CCITT T.4 and T.6 compression (types 3 and 4), 1 bit per sample</li>
* <li>LZW Compression (type 5)</li>
* <li>"Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined</li>
* <li>JPEG Compression (type 7)</li>
* <li>ZLib (aka Adobe-style Deflate) Compression (type 8)</li>
* <li>Deflate Compression (type 32946)</li>
* <li>Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression</li>
* <li>Alpha channel (ExtraSamples type 1/Associated Alpha)</li>
* <li>CMYK data (PhotometricInterpretation type 5/Separated)</li>
* <li>YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG</li>
* <li>Alpha channel (ExtraSamples types 1/Associated Alpha and 2/Unassociated Alpha)</li>
* <li>Class S, CMYK data (PhotometricInterpretation type 5/Separated)</li>
* <li>Class Y, YCbCr data (PhotometricInterpretation type 6/YCbCr for both JPEG and other compressions</li>
* <li>Planar data (PlanarConfiguration type 2/Planar)</li>
* <li>ICC profiles (ICCProfile)</li>
* <li>BitsPerSample values up to 16 for most PhotometricInterpretations</li>
@@ -119,7 +125,6 @@ public class TIFFImageReader extends ImageReaderBase {
// TODO: Implement readAsRenderedImage to allow tiled RenderedImage?
// For some layouts, we could do reads super-fast with a memory mapped buffer.
// TODO: Implement readAsRaster directly
// TODO: IIOMetadata (stay close to Sun's TIFF metadata)
// http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/package-summary.html#ImageMetadata
// TODOs Extension support
@@ -136,6 +141,7 @@ public class TIFFImageReader extends ImageReaderBase {
// Support Compression 2 (CCITT Modified Huffman RLE) for bi-level images
// Source region
// Subsampling
// IIOMetadata (stay close to Sun's TIFF metadata)
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
@@ -167,6 +173,73 @@ public class TIFFImageReader extends ImageReaderBase {
for (int i = 0; i < IFDs.directoryCount(); i++) {
System.err.printf("IFD %d: %s\n", i, IFDs.getDirectory(i));
}
Entry tiffXMP = IFDs.getEntryById(TIFF.TAG_XMP);
if (tiffXMP != null) {
byte[] value = (byte[]) tiffXMP.getValue();
// The XMPReader doesn't like null-termination...
int len = value.length;
for (int i = len - 1; i > 0; i--) {
if (value[i] == 0) {
len--;
}
else {
break;
}
}
Directory xmp = new XMPReader().read(new ByteArrayImageInputStream(value, 0, len));
System.err.println("-----------------------------------------------------------------------------");
System.err.println("xmp: " + xmp);
}
Entry tiffIPTC = IFDs.getEntryById(TIFF.TAG_IPTC);
if (tiffIPTC != null) {
Object value = tiffIPTC.getValue();
if (value instanceof short[]) {
System.err.println("short[]: " + value);
}
if (value instanceof long[]) {
// As seen in a Magick produced image...
System.err.println("long[]: " + value);
long[] longs = (long[]) value;
value = new byte[longs.length * 8];
ByteBuffer.wrap((byte[]) value).asLongBuffer().put(longs);
}
if (value instanceof float[]) {
System.err.println("float[]: " + value);
}
if (value instanceof double[]) {
System.err.println("double[]: " + value);
}
Directory iptc = new IPTCReader().read(new ByteArrayImageInputStream((byte[]) value));
System.err.println("-----------------------------------------------------------------------------");
System.err.println("iptc: " + iptc);
}
Entry tiffPSD = IFDs.getEntryById(TIFF.TAG_PHOTOSHOP);
if (tiffPSD != null) {
Directory psd = new PSDReader().read(new ByteArrayImageInputStream((byte[]) tiffPSD.getValue()));
System.err.println("-----------------------------------------------------------------------------");
System.err.println("psd: " + psd);
}
Entry tiffPSD2 = IFDs.getEntryById(TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA);
if (tiffPSD2 != null) {
byte[] value = (byte[]) tiffPSD2.getValue();
String foo = "Adobe Photoshop Document Data Block";
if (Arrays.equals(foo.getBytes(StandardCharsets.US_ASCII), Arrays.copyOf(value, foo.length()))) {
System.err.println("foo: " + foo);
// int offset = foo.length() + 1;
// ImageInputStream input = new ByteArrayImageInputStream(value, offset, value.length - offset);
// input.setByteOrder(ByteOrder.LITTLE_ENDIAN); // TODO: WHY???!
// Directory psd2 = new PSDReader().read(input);
// System.err.println("-----------------------------------------------------------------------------");
// System.err.println("psd2: " + psd2);
}
}
}
}
}
@@ -414,11 +487,17 @@ public class TIFFImageReader extends ImageReaderBase {
if (bitsPerSample == 16) {
return DataBuffer.TYPE_SHORT;
}
throw new IIOException("Unsupported BitPerSample for SampleFormat 2/Signed Integer (expected 16): " + bitsPerSample);
throw new IIOException("Unsupported BitsPerSample for SampleFormat 2/Signed Integer (expected 16): " + bitsPerSample);
case TIFFExtension.SAMPLEFORMAT_FP:
throw new IIOException("Unsupported TIFF SampleFormat: (3/Floating point)");
throw new IIOException("Unsupported TIFF SampleFormat: 3 (Floating point)");
case TIFFExtension.SAMPLEFORMAT_UNDEFINED:
throw new IIOException("Unsupported TIFF SampleFormat (4/Undefined)");
// Spec says:
// A field value of “undefined” is a statement by the writer that it did not know how
// to interpret the data samples; for example, if it were copying an existing image. A
// reader would typically treat an image with “undefined” data as if the field were
// not present (i.e. as unsigned integer data).
// TODO: We should probably issue a warning instead, and assume SAMPLEFORMAT_UINT
throw new IIOException("Unsupported TIFF SampleFormat: 4 (Undefined)");
default:
throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat);
}
@@ -888,7 +967,7 @@ public class TIFFImageReader extends ImageReaderBase {
imageInput.seek(realJPEGOffset);
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE);
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Integer.MAX_VALUE);
jpegReader.setInput(stream);
// Read data
@@ -1500,27 +1579,30 @@ public class TIFFImageReader extends ImageReaderBase {
// param.setSourceSubsampling(sub, sub, 0, 0);
// }
long start = System.currentTimeMillis();
try {
long start = System.currentTimeMillis();
// int width = reader.getWidth(imageNo);
// int height = reader.getHeight(imageNo);
// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
// param.setSourceRegion(new Rectangle(100, 300, 400, 400));
// param.setSourceRegion(new Rectangle(3, 3, 9, 9));
// param.setDestinationOffset(new Point(50, 150));
// param.setSourceSubsampling(2, 2, 0, 0);
BufferedImage image = reader.read(imageNo, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
BufferedImage image = reader.read(imageNo, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
IIOMetadata metadata = reader.getImageMetadata(imageNo);
if (metadata != null) {
if (metadata.getNativeMetadataFormatName() != null) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
IIOMetadata metadata = reader.getImageMetadata(imageNo);
if (metadata != null) {
if (metadata.getNativeMetadataFormatName() != null) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
}
/*else*/
if (metadata.isStandardMetadataFormatSupported()) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
}
}
/*else*/ if (metadata.isStandardMetadataFormatSupported()) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
}
}
System.err.println("image: " + image);
System.err.println("image: " + image);
// File tempFile = File.createTempFile("lzw-", ".bin");
// byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
@@ -1536,7 +1618,7 @@ public class TIFFImageReader extends ImageReaderBase {
//
// System.err.println("tempFile: " + tempFile.getAbsolutePath());
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
// image = new ResampleOp(reader.getWidth(0) / 4, reader.getHeight(0) / 4, ResampleOp.FILTER_LANCZOS).filter(image, null);
//
// int maxW = 800;
// int maxH = 800;
@@ -1553,30 +1635,35 @@ public class TIFFImageReader extends ImageReaderBase {
// // System.err.println("Scale time: " + (System.currentTimeMillis() - start) + " ms");
// }
if (image.getType() == BufferedImage.TYPE_CUSTOM) {
start = System.currentTimeMillis();
image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));
System.err.println("Conversion time: " + (System.currentTimeMillis() - start) + " ms");
}
if (image.getType() == BufferedImage.TYPE_CUSTOM) {
start = System.currentTimeMillis();
image = new ColorConvertOp(null).filter(image, new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB));
System.err.println("Conversion time: " + (System.currentTimeMillis() - start) + " ms");
}
showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo)));
showIt(image, String.format("Image: %s [%d x %d]", file.getName(), reader.getWidth(imageNo), reader.getHeight(imageNo)));
try {
int numThumbnails = reader.getNumThumbnails(0);
for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) {
BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo);
// System.err.println("thumbnail: " + thumbnail);
showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
try {
int numThumbnails = reader.getNumThumbnails(0);
for (int thumbnailNo = 0; thumbnailNo < numThumbnails; thumbnailNo++) {
BufferedImage thumbnail = reader.readThumbnail(imageNo, thumbnailNo);
// System.err.println("thumbnail: " + thumbnail);
showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
}
}
catch (IIOException e) {
System.err.println("Could not read thumbnails: " + e.getMessage());
e.printStackTrace();
}
}
catch (IIOException e) {
System.err.println("Could not read thumbnails: " + e.getMessage());
e.printStackTrace();
catch (Throwable t) {
System.err.println(file + " image " + imageNo + " can't be read:");
t.printStackTrace();
}
}
}
catch (Throwable t) {
System.err.println(file);
System.err.println(file + " can't be read:");
t.printStackTrace();
}
finally {
@@ -31,6 +31,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.metadata.AbstractEntry;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFWriter;
import com.twelvemonkeys.imageio.metadata.exif.Rational;
@@ -39,9 +40,12 @@ import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.io.enc.PackBitsEncoder;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.*;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
@@ -50,10 +54,9 @@ import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.image.*;
import java.io.*;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.*;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
@@ -65,13 +68,16 @@ import java.util.zip.DeflaterOutputStream;
* @version $Id: TIFFImageWriter.java,v 1.0 18.09.13 12:46 haraldk Exp$
*/
public final class TIFFImageWriter extends ImageWriterBase {
// Short term
// TODO: Support more of the ImageIO metadata (ie. compression from metadata, etc)
// Long term
// TODO: Support tiling
// TODO: Support thumbnails
// TODO: Support ImageIO metadata
// TODO: Support CCITT Modified Huffman compression (2)
// TODO: Full "Baseline TIFF" support (pending CCITT compression 2)
// TODO: CCITT compressions T.4 and T.6
// TODO: Support JPEG compression of CMYK data (pending JPEGImageWriter CMYK write support)
// ----
// TODO: Support storing multiple images in one stream (multi-page TIFF)
// TODO: Support use-case: Transcode multi-layer PSD to multi-page TIFF with metadata
@@ -91,6 +97,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
// Support LZW compression (5)
// Support JPEG compression (7) - might need extra input to allow multiple images with single DQT
// Use sensible defaults for compression based on input? None is sensible... :-)
// Support resolution, resolution unit and software tags from ImageIO metadata
public static final Rational STANDARD_DPI = new Rational(72);
@@ -98,14 +105,81 @@ public final class TIFFImageWriter extends ImageWriterBase {
super(provider);
}
@Override
public void setOutput(final Object output) {
super.setOutput(output);
// TODO: Allow appending/partly overwrite of existing file...
}
static final class TIFFEntry extends AbstractEntry {
TIFFEntry(Object identifier, Object value) {
// TODO: Expose a merge of this and the EXIFEntry class...
private final short type;
private static short guessType(final Object val) {
// TODO: This code is duplicated in EXIFWriter.getType, needs refactor!
Object value = Validate.notNull(val);
boolean array = value.getClass().isArray();
if (array) {
value = Array.get(value, 0);
}
// Note: This "narrowing" is to keep data consistent between read/write.
// TODO: Check for negative values and use signed types?
if (value instanceof Byte) {
return TIFF.TYPE_BYTE;
}
if (value instanceof Short) {
if (!array && (Short) value < Byte.MAX_VALUE) {
return TIFF.TYPE_BYTE;
}
return TIFF.TYPE_SHORT;
}
if (value instanceof Integer) {
if (!array && (Integer) value < Short.MAX_VALUE) {
return TIFF.TYPE_SHORT;
}
return TIFF.TYPE_LONG;
}
if (value instanceof Long) {
if (!array && (Long) value < Integer.MAX_VALUE) {
return TIFF.TYPE_LONG;
}
}
if (value instanceof Rational) {
return TIFF.TYPE_RATIONAL;
}
if (value instanceof String) {
return TIFF.TYPE_ASCII;
}
// TODO: More types
throw new UnsupportedOperationException(String.format("Method guessType not implemented for value of type %s", value.getClass()));
}
TIFFEntry(final int identifier, final Object value) {
this(identifier, guessType(value), value);
}
TIFFEntry(int identifier, short type, Object value) {
super(identifier, value);
this.type = type;
}
@Override
public String getTypeName() {
return TIFF.TYPE_NAMES[type];
}
}
@Override
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException {
// TODO: Validate input
assertOutput();
@@ -120,6 +194,14 @@ public final class TIFFImageWriter extends ImageWriterBase {
ColorModel colorModel = renderedImage.getColorModel();
int numComponents = colorModel.getNumComponents();
TIFFImageMetadata metadata;
if (image.getMetadata() != null) {
metadata = convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
}
else {
metadata = initMeta(null, ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
}
SampleModel sampleModel = renderedImage.getSampleModel();
int[] bandOffsets;
@@ -140,7 +222,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
List<Entry> entries = new ArrayList<>();
Set<Entry> entries = new LinkedHashSet<>();
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
// entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
@@ -157,10 +239,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
}
// Write compression field from param or metadata
// TODO: Support COPY_FROM_METADATA
int compression = TIFFImageWriteParam.getCompressionType(param);
entries.add(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:
@@ -169,7 +253,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
default:
}
// TODO: We might want to support CMYK in JPEG as well...
// TODO: We might want to support CMYK in JPEG as well... Pending JPEG CMYK write support.
int photometric = compression == TIFFExtension.COMPRESSION_JPEG ?
TIFFExtension.PHOTOMETRIC_YCBCR :
getPhotometricInterpretation(colorModel);
@@ -189,15 +273,24 @@ public final class TIFFImageWriter extends ImageWriterBase {
}
}
// 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.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
// TODO: Float values!
entries.add(new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer")); // TODO: Get from metadata (optional) + fill in version number
// Get Software from metadata, or use default
Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE);
entries.add(software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
entries.add(new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
entries.add(new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
entries.add(new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
// 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.add(xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION);
entries.add(yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT);
entries.add(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.add(new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended
@@ -208,7 +301,8 @@ public final class TIFFImageWriter extends ImageWriterBase {
TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1);
entries.add(dummyStripOffsets); // Updated later
// TODO: If tiled, write tile indexes etc, or always do that?
// TODO: If tiled, write tile indexes etc
// Depending on param.getTilingMode
EXIFWriter exifWriter = new EXIFWriter();
@@ -233,6 +327,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
// TODO: Create compressor stream per Tile/Strip
if (compression == TIFFExtension.COMPRESSION_JPEG) {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("JPEG");
if (!writers.hasNext()) {
// This can only happen if someone deliberately uninstalled it
throw new IIOException("No JPEG ImageWriter found!");
@@ -607,13 +702,75 @@ public final class TIFFImageWriter extends ImageWriterBase {
// Metadata
@Override
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
return null;
public TIFFImageMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
return initMeta(null, imageType, param);
}
@Override
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
return null;
public TIFFImageMetadata convertImageMetadata(final IIOMetadata inData,
final ImageTypeSpecifier imageType,
final ImageWriteParam param) {
Validate.notNull(inData, "inData");
Validate.notNull(imageType, "imageType");
Directory ifd;
if (inData instanceof TIFFImageMetadata) {
ifd = ((TIFFImageMetadata) inData).getIFD();
}
else {
TIFFImageMetadata outData = new TIFFImageMetadata(Collections.<Entry>emptySet());
try {
if (Arrays.asList(inData.getMetadataFormatNames()).contains(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME)) {
outData.setFromTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, inData.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME));
}
else if (inData.isStandardMetadataFormatSupported()) {
outData.setFromTree(IIOMetadataFormatImpl.standardMetadataFormatName, inData.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName));
}
else {
// Unknown format, we can't convert it
return null;
}
}
catch (IIOInvalidTreeException e) {
// TODO: How to issue warning when warning requires imageIndex??? Use -1?
}
ifd = outData.getIFD();
}
// Overwrite in values with values from imageType and param as needed
return initMeta(ifd, imageType, param);
}
private TIFFImageMetadata initMeta(final Directory ifd, final ImageTypeSpecifier imageType, final ImageWriteParam param) {
Validate.notNull(imageType, "imageType");
Map<Integer, Entry> entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20);
if (ifd != null) {
for (Entry entry : ifd) {
entries.put((Integer) entry.getIdentifier(), entry);
}
}
// TODO: Set values from imageType
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, getPhotometricInterpretation(imageType.getColorModel())));
// TODO: Set values from param if != null + combined values...
return new TIFFImageMetadata(entries.values());
}
@Override
public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam param) {
return super.getDefaultStreamMetadata(param);
}
@Override
public IIOMetadata convertStreamMetadata(final IIOMetadata inData, final ImageWriteParam param) {
return super.convertStreamMetadata(inData, param);
}
// Param
@@ -762,5 +919,4 @@ public final class TIFFImageWriter extends ImageWriterBase {
TIFFImageReader.showIt(read, output.getName());
}
}