diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java index 202a8357..d8ff9607 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/AbstractMetadata.java @@ -98,15 +98,6 @@ public abstract class AbstractMetadata extends IIOMetadata implements Cloneable if (!root.getNodeName().equals(formatName)) { throw new IIOInvalidTreeException("Root must be " + formatName, root); } - - // TODO: Merge both native and standard! - Node node = root.getFirstChild(); - while (node != null) { - // TODO: Merge values from node into this - - // Move to the next sibling - node = node.getNextSibling(); - } } @Override diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java index 67ecd60f..15f9a31d 100755 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageWriterAbstractTestCase.java @@ -63,6 +63,7 @@ public abstract class ImageWriterAbstractTestCase { static { IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + ImageIO.setUseCache(false); } protected abstract ImageWriter createImageWriter(); @@ -120,23 +121,20 @@ public abstract class ImageWriterAbstractTestCase { for (RenderedImage testData : getTestData()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - ImageOutputStream stream = ImageIO.createImageOutputStream(buffer); - writer.setOutput(stream); - try { + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); writer.write(drawSomething((BufferedImage) testData)); } catch (IOException e) { fail(e.getMessage()); } - finally { - stream.close(); // Force data to be written - } assertTrue("No image data written", buffer.size() > 0); } } + @SuppressWarnings("ConstantConditions") @Test public void testWriteNull() throws IOException { ImageWriter writer = createImageWriter(); diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java index 5b6e44c2..c0fe8f94 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadata.java @@ -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 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 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 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 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 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 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 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 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 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 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 entries) { + super(entries); + } + } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index b5052f3e..576f9413 100755 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -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: *
    *
  • Tiling
  • + *
  • Class F (Facsimile), CCITT T.4 and T.6 compression (types 3 and 4), 1 bit per sample
  • *
  • LZW Compression (type 5)
  • *
  • "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined
  • *
  • JPEG Compression (type 7)
  • *
  • ZLib (aka Adobe-style Deflate) Compression (type 8)
  • *
  • Deflate Compression (type 32946)
  • *
  • Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression
  • - *
  • Alpha channel (ExtraSamples type 1/Associated Alpha)
  • - *
  • CMYK data (PhotometricInterpretation type 5/Separated)
  • - *
  • YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG
  • + *
  • Alpha channel (ExtraSamples types 1/Associated Alpha and 2/Unassociated Alpha)
  • + *
  • Class S, CMYK data (PhotometricInterpretation type 5/Separated)
  • + *
  • Class Y, YCbCr data (PhotometricInterpretation type 6/YCbCr for both JPEG and other compressions
  • *
  • Planar data (PlanarConfiguration type 2/Planar)
  • *
  • ICC profiles (ICCProfile)
  • *
  • BitsPerSample values up to 16 for most PhotometricInterpretations
  • @@ -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 { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index b771f005..b4012e0b 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -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 entries = new ArrayList<>(); + Set 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 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.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 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()); } - } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java new file mode 100644 index 00000000..87d1f221 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageMetadataTest.java @@ -0,0 +1,599 @@ +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.imageio.metadata.Directory; +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.stream.URLImageInputStreamSpi; +import com.twelvemonkeys.lang.StringUtil; +import org.junit.Test; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.imageio.ImageIO; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Collections; + +import static org.junit.Assert.*; + +/** + * TIFFImageMetadataTest. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: TIFFImageMetadataTest.java,v 1.0 30/07/15 harald.kuhr Exp$ + */ +public class TIFFImageMetadataTest { + + static { + IIORegistry.getDefaultInstance().registerServiceProvider(new URLImageInputStreamSpi()); + ImageIO.setUseCache(false); + } + + // TODO: Candidate super method + private URL getClassLoaderResource(final String resource) { + return getClass().getResource(resource); + } + + // TODO: Candidate abstract super method + private IIOMetadata createMetadata(final String resource) throws IOException { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource(resource))) { + Directory ifd = new EXIFReader().read(input); +// System.err.println("ifd: " + ifd); + return new TIFFImageMetadata(ifd); + } + } + + @Test + public void testMetadataStandardFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/smallliz.tif"); + Node root = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + + // Root: "javax_imageio_1.0" + assertNotNull(root); + assertEquals(IIOMetadataFormatImpl.standardMetadataFormatName, root.getNodeName()); + assertEquals(6, root.getChildNodes().getLength()); + + // "Chroma" + Node chroma = root.getFirstChild(); + assertEquals("Chroma", chroma.getNodeName()); + + assertEquals(3, chroma.getChildNodes().getLength()); + + Node colorSpaceType = chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("YCbCr", ((Element) colorSpaceType).getAttribute("value")); + + Node numChannels = colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("3", ((Element) numChannels).getAttribute("value")); + + Node blackIsZero = numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals(0, blackIsZero.getAttributes().getLength()); + + // "Compression" + Node compression = chroma.getNextSibling(); + assertEquals("Compression", compression.getNodeName()); + assertEquals(2, compression.getChildNodes().getLength()); + + Node compressionTypeName = compression.getFirstChild(); + assertEquals("CompressionTypeName", compressionTypeName.getNodeName()); + assertEquals("Old JPEG", ((Element) compressionTypeName).getAttribute("value")); + + Node lossless = compressionTypeName.getNextSibling(); + assertEquals("Lossless", lossless.getNodeName()); + assertEquals("FALSE", ((Element) lossless).getAttribute("value")); + + // "Data" + Node data = compression.getNextSibling(); + assertEquals("Data", data.getNodeName()); + assertEquals(4, data.getChildNodes().getLength()); + + Node planarConfiguration = data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", ((Element) planarConfiguration).getAttribute("value")); + + Node sampleFormat = planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFormat.getNodeName()); + assertEquals("UnsignedIntegral", ((Element) sampleFormat).getAttribute("value")); + + Node bitsPerSample = sampleFormat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8 8 8", ((Element) bitsPerSample).getAttribute("value")); + + Node sampleMSB = bitsPerSample.getNextSibling(); + assertEquals("SampleMSB", sampleMSB.getNodeName()); + assertEquals("0 0 0", ((Element) sampleMSB).getAttribute("value")); + + // "Dimension" + Node dimension = data.getNextSibling(); + assertEquals("Dimension", dimension.getNodeName()); + assertEquals(3, dimension.getChildNodes().getLength()); + + Node pixelAspectRatio = dimension.getFirstChild(); + assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); + assertEquals("1.0", ((Element) pixelAspectRatio).getAttribute("value")); + + Node horizontalPixelSize = pixelAspectRatio.getNextSibling(); + assertEquals("HorizontalPixelSize", horizontalPixelSize.getNodeName()); + assertEquals("0.254", ((Element) horizontalPixelSize).getAttribute("value")); + + Node verticalPixelSize = horizontalPixelSize.getNextSibling(); + assertEquals("VerticalPixelSize", verticalPixelSize.getNodeName()); + assertEquals("0.254", ((Element) verticalPixelSize).getAttribute("value")); + + // "Document" + Node document = dimension.getNextSibling(); + assertEquals("Document", document.getNodeName()); + assertEquals(1, document.getChildNodes().getLength()); + + Node formatVersion = document.getFirstChild(); + assertEquals("FormatVersion", formatVersion.getNodeName()); + assertEquals("6.0", ((Element) formatVersion).getAttribute("value")); + + // "Text" + Node text = document.getNextSibling(); + assertEquals("Text", text.getNodeName()); + assertEquals(1, text.getChildNodes().getLength()); + + // NOTE: Could be multiple "TextEntry" elements, with different "keyword" attributes + Node textEntry = text.getFirstChild(); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("Software", ((Element) textEntry).getAttribute("keyword")); + assertEquals("HP IL v1.1", ((Element) textEntry).getAttribute("value")); + } + + @Test + public void testMetadataNativeFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/quad-lzw.tif"); + Node root = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + + // Root: "com_sun_media_imageio_plugins_tiff_image_1.0" + assertNotNull(root); + assertEquals(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME, root.getNodeName()); + assertEquals(1, root.getChildNodes().getLength()); + + // IFD: "TIFFIFD" + Node ifd = root.getFirstChild(); + assertEquals("TIFFIFD", ifd.getNodeName()); + + NodeList entries = ifd.getChildNodes(); + assertEquals(13, entries.getLength()); + + String[] stripOffsets = { + "8", "150", "292", "434", "576", "718", "860", "1002", "1144", "1286", + "1793", "3823", "7580", "12225", "17737", "23978", "30534", "36863", "42975", "49180", + "55361", "61470", "67022", "71646", "74255", "75241", "75411", "75553", "75695", "75837", + "75979", "76316", "77899", "80466", "84068", "88471", "93623", "99105", "104483", "109663", + "114969", "120472", "126083", "131289", "135545", "138810", "140808", "141840", "141982", "142124", + "142266", "142408", "142615", "144074", "146327", "149721", "154066", "158927", "164022", "169217", + "174409", "179657", "185166", "190684", "196236", "201560", "206064", "209497", "211612", "212419", + "212561", "212703", "212845", "212987", "213129", "213271", "213413" + }; + + String[] stripByteCounts = { + "142", "142", "142", "142", "142", "142", "142", "142", "142", "507", + "2030", "3757", "4645", "5512", "6241", "6556", "6329", "6112", "6205", "6181", + "6109", "5552", "4624", "2609", "986", "170", "142", "142", "142", "142", + "337", "1583", "2567", "3602", "4403", "5152", "5482", "5378", "5180", "5306", + "5503", "5611", "5206", "4256", "3265", "1998", "1032", "142", "142", "142", + "142", "207", "1459", "2253", "3394", "4345", "4861", "5095", "5195", "5192", + "5248", "5509", "5518", "5552", "5324", "4504", "3433", "2115", "807", "142", + "142", "142", "142", "142", "142", "142", "128" + }; + + // The 13 entries + assertSingleNodeWithValue(entries, TIFF.TAG_IMAGE_WIDTH, TIFF.TYPE_SHORT, "512"); + assertSingleNodeWithValue(entries, TIFF.TAG_IMAGE_HEIGHT, TIFF.TYPE_SHORT, "384"); + assertSingleNodeWithValue(entries, TIFF.TAG_BITS_PER_SAMPLE, TIFF.TYPE_SHORT, "8", "8", "8"); + assertSingleNodeWithValue(entries, TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, "5"); + assertSingleNodeWithValue(entries, TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, "2"); + assertSingleNodeWithValue(entries, TIFF.TAG_STRIP_OFFSETS, TIFF.TYPE_LONG, stripOffsets); + assertSingleNodeWithValue(entries, TIFF.TAG_SAMPLES_PER_PIXEL, TIFF.TYPE_SHORT, "3"); + assertSingleNodeWithValue(entries, TIFF.TAG_ROWS_PER_STRIP, TIFF.TYPE_LONG, "5"); + assertSingleNodeWithValue(entries, TIFF.TAG_STRIP_BYTE_COUNTS, TIFF.TYPE_LONG, stripByteCounts); + assertSingleNodeWithValue(entries, TIFF.TAG_PLANAR_CONFIGURATION, TIFF.TYPE_SHORT, "1"); + assertSingleNodeWithValue(entries, TIFF.TAG_X_POSITION, TIFF.TYPE_RATIONAL, "0"); + assertSingleNodeWithValue(entries, TIFF.TAG_Y_POSITION, TIFF.TYPE_RATIONAL, "0"); + assertSingleNodeWithValue(entries, 32995, TIFF.TYPE_SHORT, "0"); // Matteing tag, obsoleted by ExtraSamples tag in TIFF 6.0 + } + + @Test + public void testTreeDetached() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + Node nativeTree = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + assertNotNull(nativeTree); + + Node nativeTree2 = metadata.getAsTree(TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME); + assertNotNull(nativeTree2); + + assertNotSame(nativeTree, nativeTree2); + assertNodeEquals("Unmodified trees differs", nativeTree, nativeTree2); // Both not modified + + // Modify one of the trees + Node ifdNode = nativeTree2.getFirstChild(); + ifdNode.removeChild(ifdNode.getFirstChild()); + IIOMetadataNode tiffField = new IIOMetadataNode("TIFFField"); + ifdNode.appendChild(tiffField); + + assertNodeNotEquals("Modified tree does not differ", nativeTree, nativeTree2); + + } + + @Test + public void testMergeTree() throws IOException { + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + + Node nativeTree = metadata.getAsTree(nativeFormat); + assertNotNull(nativeTree); + + IIOMetadataNode newTree = new IIOMetadataNode("com_sun_media_imageio_plugins_tiff_image_1.0"); + IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD"); + newTree.appendChild(ifdNode); + + createTIFFFieldNode(ifdNode, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, TIFFBaseline.RESOLUTION_UNIT_DPI); + createTIFFFieldNode(ifdNode, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, new Rational(300)); + createTIFFFieldNode(ifdNode, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, new Rational(30001, 100)); + + metadata.mergeTree(nativeFormat, newTree); + + Directory ifd = metadata.getIFD(); + + assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION)); + assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION)); + assertEquals(new Rational(30001, 100), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT)); + assertEquals(TIFFBaseline.RESOLUTION_UNIT_DPI, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue()); + + Node mergedTree = metadata.getAsTree(nativeFormat); + NodeList fields = mergedTree.getFirstChild().getChildNodes(); + + // Validate there's one and only one resolution unit, x res and y res + // Validate resolution unit == 1, x res & y res + assertSingleNodeWithValue(fields, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, String.valueOf(TIFFBaseline.RESOLUTION_UNIT_DPI)); + assertSingleNodeWithValue(fields, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, "300"); + assertSingleNodeWithValue(fields, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, "30001/100"); + } + + @Test + public void testMergeTreeStandardFormat() throws IOException { + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/zackthecat.tif"); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + + Node standardTree = metadata.getAsTree(standardFormat); + assertNotNull(standardTree); + + IIOMetadataNode newTree = new IIOMetadataNode(standardFormat); + IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension"); + newTree.appendChild(dimensionNode); + + IIOMetadataNode horizontalPixelSize = new IIOMetadataNode("HorizontalPixelSize"); + dimensionNode.appendChild(horizontalPixelSize); + horizontalPixelSize.setAttribute("value", String.valueOf(300 / 25.4)); + + IIOMetadataNode verticalPixelSize = new IIOMetadataNode("VerticalPixelSize"); + dimensionNode.appendChild(verticalPixelSize); + verticalPixelSize.setAttribute("value", String.valueOf(300 / 25.4)); + + metadata.mergeTree(standardFormat, newTree); + + Directory ifd = metadata.getIFD(); + + assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION)); + assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION)); + assertEquals(new Rational(300), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue()); + + // Should keep DPI as unit + assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT)); + assertEquals(TIFFBaseline.RESOLUTION_UNIT_DPI, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue()); + } + + @Test + public void testMergeTreeStandardFormatAspectOnly() throws IOException { + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif"); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + + Node standardTree = metadata.getAsTree(standardFormat); + assertNotNull(standardTree); + + IIOMetadataNode newTree = new IIOMetadataNode(standardFormat); + IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension"); + newTree.appendChild(dimensionNode); + + IIOMetadataNode aspectRatio = new IIOMetadataNode("PixelAspectRatio"); + dimensionNode.appendChild(aspectRatio); + aspectRatio.setAttribute("value", String.valueOf(3f / 2f)); + + metadata.mergeTree(standardFormat, newTree); + + Directory ifd = metadata.getIFD(); + + assertNotNull(ifd.getEntryById(TIFF.TAG_X_RESOLUTION)); + assertEquals(new Rational(3, 2), ifd.getEntryById(TIFF.TAG_X_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_Y_RESOLUTION)); + assertEquals(new Rational(1), ifd.getEntryById(TIFF.TAG_Y_RESOLUTION).getValue()); + assertNotNull(ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT)); + assertEquals(TIFFBaseline.RESOLUTION_UNIT_NONE, ((Number) ifd.getEntryById(TIFF.TAG_RESOLUTION_UNIT).getValue()).intValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMergeTreeUnsupportedFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = "com_foo_bar_tiff_42"; + metadata.mergeTree(nativeFormat, new IIOMetadataNode(nativeFormat)); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testMergeTreeFormatMisMatch() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.mergeTree(nativeFormat, new IIOMetadataNode("com_foo_bar_tiff_42")); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testMergeTreeInvalid() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.mergeTree(nativeFormat, new IIOMetadataNode(nativeFormat)); // Requires at least one child node + } + + // TODO: Test that failed merge leaves metadata unchanged + + @Test + public void testSetFromTreeEmpty() throws IOException { + // Read from file, set empty to see that all is cleared + TIFFImageMetadata metadata = (TIFFImageMetadata) createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadataNode root = new IIOMetadataNode(nativeFormat); + root.appendChild(new IIOMetadataNode("TIFFIFD")); + + metadata.setFromTree(nativeFormat, root); + + Directory ifd = metadata.getIFD(); + assertNotNull(ifd); + assertEquals(0, ifd.size()); + + Node tree = metadata.getAsTree(nativeFormat); + + assertNotNull(tree); + assertNotNull(tree.getFirstChild()); + assertEquals(1, tree.getChildNodes().getLength()); + } + + @Test + public void testSetFromTree() throws IOException { + String softwareString = "12M UberTIFF 1.0"; + + TIFFImageMetadata metadata = new TIFFImageMetadata(Collections.emptySet()); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadataNode root = new IIOMetadataNode(nativeFormat); + + IIOMetadataNode ifdNode = new IIOMetadataNode("TIFFIFD"); + root.appendChild(ifdNode); + + createTIFFFieldNode(ifdNode, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString); + + metadata.setFromTree(nativeFormat, root); + + Directory ifd = metadata.getIFD(); + assertNotNull(ifd); + assertEquals(1, ifd.size()); + + assertNotNull(ifd.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals(softwareString, ifd.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + + Node tree = metadata.getAsTree(nativeFormat); + + assertNotNull(tree); + assertNotNull(tree.getFirstChild()); + assertEquals(1, tree.getChildNodes().getLength()); + } + + @Test + public void testSetFromTreeStandardFormat() throws IOException { + String softwareString = "12M UberTIFF 1.0"; + String copyrightString = "Copyright (C) TwelveMonkeys, 2015"; + + TIFFImageMetadata metadata = new TIFFImageMetadata(Collections.emptySet()); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + IIOMetadataNode root = new IIOMetadataNode(standardFormat); + + IIOMetadataNode textNode = new IIOMetadataNode("Text"); + root.appendChild(textNode); + + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + textNode.appendChild(textEntry); + + textEntry.setAttribute("keyword", "SOFTWARE"); // Spelling should not matter + textEntry.setAttribute("value", softwareString); + + textEntry = new IIOMetadataNode("TextEntry"); + textNode.appendChild(textEntry); + + textEntry.setAttribute("keyword", "copyright"); // Spelling should not matter + textEntry.setAttribute("value", copyrightString); + + metadata.setFromTree(standardFormat, root); + + Directory ifd = metadata.getIFD(); + assertNotNull(ifd); + assertEquals(2, ifd.size()); + + assertNotNull(ifd.getEntryById(TIFF.TAG_SOFTWARE)); + assertEquals(softwareString, ifd.getEntryById(TIFF.TAG_SOFTWARE).getValue()); + + assertNotNull(ifd.getEntryById(TIFF.TAG_COPYRIGHT)); + assertEquals(copyrightString, ifd.getEntryById(TIFF.TAG_COPYRIGHT).getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetFromTreeUnsupportedFormat() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = "com_foo_bar_tiff_42"; + metadata.setFromTree(nativeFormat, new IIOMetadataNode(nativeFormat)); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testSetFromTreeFormatMisMatch() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.setFromTree(nativeFormat, new IIOMetadataNode("com_foo_bar_tiff_42")); + } + + @Test(expected = IIOInvalidTreeException.class) + public void testSetFromTreeInvalid() throws IOException { + IIOMetadata metadata = createMetadata("/tiff/sm_colors_tile.tif"); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + metadata.setFromTree(nativeFormat, new IIOMetadataNode(nativeFormat)); // Requires at least one child node + } + + private void assertSingleNodeWithValue(final NodeList fields, final int tag, int type, final String... expectedValue) { + String tagNumber = String.valueOf(tag); + String typeName = StringUtil.capitalize(TIFF.TYPE_NAMES[type].toLowerCase()); + + boolean foundTag = false; + + for (int i = 0; i < fields.getLength(); i++) { + Element field = (Element) fields.item(i); + + if (tagNumber.equals(field.getAttribute("number"))) { + assertFalse("Duplicate tag " + tagNumber + " found", foundTag); + + assertEquals(1, field.getChildNodes().getLength()); + Node containerNode = field.getFirstChild(); + assertEquals("TIFF" + typeName + "s", containerNode.getNodeName()); + + NodeList valueNodes = containerNode.getChildNodes(); + assertEquals("Unexpected number of values for tag " + tagNumber, expectedValue.length, valueNodes.getLength()); + + for (int j = 0; j < expectedValue.length; j++) { + Element valueNode = (Element) valueNodes.item(j); + assertEquals("TIFF" + typeName, valueNode.getNodeName()); + assertEquals("Unexpected tag " + tagNumber + " value", expectedValue[j], valueNode.getAttribute("value")); + } + + foundTag = true; + } + } + + assertTrue("No tag " + tagNumber + " found", foundTag); + } + + // TODO: Test that failed set leaves metadata unchanged + + static void createTIFFFieldNode(final IIOMetadataNode parentIFDNode, int tag, short type, Object value) { + IIOMetadataNode fieldNode = new IIOMetadataNode("TIFFField"); + parentIFDNode.appendChild(fieldNode); + + fieldNode.setAttribute("number", String.valueOf(tag)); + + switch (type) { + case TIFF.TYPE_ASCII: + createTIFFFieldContainerNode(fieldNode, "Ascii", value); + break; + case TIFF.TYPE_BYTE: + createTIFFFieldContainerNode(fieldNode, "Byte", value); + break; + case TIFF.TYPE_SHORT: + createTIFFFieldContainerNode(fieldNode, "Short", value); + break; + case TIFF.TYPE_RATIONAL: + createTIFFFieldContainerNode(fieldNode, "Rational", value); + break; + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + static void createTIFFFieldContainerNode(final IIOMetadataNode fieldNode, final String type, final Object value) { + IIOMetadataNode containerNode = new IIOMetadataNode("TIFF" + type + "s"); + fieldNode.appendChild(containerNode); + + IIOMetadataNode valueNode = new IIOMetadataNode("TIFF" + type); + valueNode.setAttribute("value", String.valueOf(value)); + containerNode.appendChild(valueNode); + } + + private void assertNodeNotEquals(final String message, final Node expected, final Node actual) { + // Lame, lazy implementation... + try { + assertNodeEquals(message, expected, actual); + } + catch (AssertionError ignore) { + return; + } + + fail(message); + } + + private void assertNodeEquals(final String message, final Node expected, final Node actual) { + assertEquals(message + " class differs", expected.getClass(), actual.getClass()); + assertEquals(message, expected.getNodeValue(), actual.getNodeValue()); + + if (expected instanceof IIOMetadataNode) { + IIOMetadataNode expectedIIO = (IIOMetadataNode) expected; + IIOMetadataNode actualIIO = (IIOMetadataNode) actual; + + assertEquals(message, expectedIIO.getUserObject(), actualIIO.getUserObject()); + } + + NodeList expectedChildNodes = expected.getChildNodes(); + NodeList actualChildNodes = actual.getChildNodes(); + + assertEquals(message + " child length differs: " + toString(expectedChildNodes) + " != " + toString(actualChildNodes), + expectedChildNodes.getLength(), actualChildNodes.getLength()); + + for (int i = 0; i < expectedChildNodes.getLength(); i++) { + Node expectedChild = expectedChildNodes.item(i); + Node actualChild = actualChildNodes.item(i); + + assertEquals(message + " node name differs", expectedChild.getLocalName(), actualChild.getLocalName()); + assertNodeEquals(message + "/" + expectedChild.getLocalName(), expectedChild, actualChild); + } + } + + private String toString(final NodeList list) { + if (list.getLength() == 0) { + return "[]"; + } + + StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < list.getLength(); i++) { + if (i > 0) { + builder.append(", "); + } + + Node node = list.item(i); + builder.append(node.getLocalName()); + } + builder.append("]"); + + return builder.toString(); + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java index 8c42c5a9..c775f3d4 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java @@ -28,15 +28,33 @@ package com.twelvemonkeys.imageio.plugins.tiff; +import com.twelvemonkeys.imageio.metadata.Directory; +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.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; +import org.junit.Test; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriter; -import java.awt.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageOutputStream; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.Arrays; import java.util.List; +import static com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadataTest.createTIFFFieldNode; +import static org.junit.Assert.*; + /** * TIFFImageWriterTest * @@ -55,19 +73,208 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { @Override protected List getTestData() { - BufferedImage image = new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = image.createGraphics(); - try { - graphics.setColor(Color.RED); - graphics.fillRect(0, 0, 100, 200); - graphics.setColor(Color.BLUE); - graphics.fillRect(100, 0, 100, 200); - graphics.clearRect(200, 0, 100, 200); + return Arrays.asList( + new BufferedImage(300, 200, BufferedImage.TYPE_INT_RGB), + new BufferedImage(300, 200, BufferedImage.TYPE_INT_ARGB), + new BufferedImage(300, 200, BufferedImage.TYPE_3BYTE_BGR), + new BufferedImage(300, 200, BufferedImage.TYPE_4BYTE_ABGR), + new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY), + new BufferedImage(300, 200, BufferedImage.TYPE_USHORT_GRAY), +// new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_BINARY), // TODO! + new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_INDEXED) + ); + } + + // TODO: Test write bilevel stays bilevel + // TODO: Test write indexed stays indexed + + @Test + public void testWriteWithCustomResolutionNative() throws IOException { + // Issue 139 Writing TIFF files with custom resolution value + Rational resolutionValue = new Rational(1200); + int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER; + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat); + + IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); + customMeta.appendChild(ifd); + + createTIFFFieldNode(ifd, TIFF.TAG_RESOLUTION_UNIT, TIFF.TYPE_SHORT, resolutionUnitValue); + createTIFFFieldNode(ifd, TIFF.TAG_X_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue); + createTIFFFieldNode(ifd, TIFF.TAG_Y_RESOLUTION, TIFF.TYPE_RATIONAL, resolutionValue); + + metadata.mergeTree(nativeFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); } - finally { - graphics.dispose(); + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); } - return Arrays.asList(image); + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + + Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT); + assertNotNull(resolutionUnit); + assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue()); + + Entry xResolution = ifds.getEntryById(TIFF.TAG_X_RESOLUTION); + assertNotNull(xResolution); + assertEquals(resolutionValue, xResolution.getValue()); + + Entry yResolution = ifds.getEntryById(TIFF.TAG_Y_RESOLUTION); + assertNotNull(yResolution); + assertEquals(resolutionValue, yResolution.getValue()); + } + + @Test + public void testWriteWithCustomSoftwareNative() throws IOException { + String softwareString = "12M TIFF Test 1.0 (build $foo$)"; + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String nativeFormat = TIFFMedataFormat.SUN_NATIVE_IMAGE_METADATA_FORMAT_NAME; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(nativeFormat); + + IIOMetadataNode ifd = new IIOMetadataNode("TIFFIFD"); + customMeta.appendChild(ifd); + + createTIFFFieldNode(ifd, TIFF.TAG_SOFTWARE, TIFF.TYPE_ASCII, softwareString); + + metadata.mergeTree(nativeFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); + } + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE); + assertNotNull(software); + assertEquals(softwareString, software.getValueAsString()); + } + + @Test + public void testWriteWithCustomResolutionStandard() throws IOException { + // Issue 139 Writing TIFF files with custom resolution value + double resolutionValue = 300 / 25.4; // 300 dpi, 1 inch = 2.54 cm or 25.4 mm + int resolutionUnitValue = TIFFBaseline.RESOLUTION_UNIT_CENTIMETER; + Rational expectedResolutionValue = new Rational(Math.round(resolutionValue * 10 * TIFFImageMetadata.RATIONAL_SCALE_FACTOR), TIFFImageMetadata.RATIONAL_SCALE_FACTOR); + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat); + + IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); + customMeta.appendChild(dimension); + + IIOMetadataNode xSize = new IIOMetadataNode("HorizontalPixelSize"); + dimension.appendChild(xSize); + xSize.setAttribute("value", String.valueOf(resolutionValue)); + + IIOMetadataNode ySize = new IIOMetadataNode("VerticalPixelSize"); + dimension.appendChild(ySize); + ySize.setAttribute("value", String.valueOf(resolutionValue)); + + metadata.mergeTree(standardFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); + } + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + + Entry resolutionUnit = ifds.getEntryById(TIFF.TAG_RESOLUTION_UNIT); + assertNotNull(resolutionUnit); + assertEquals(resolutionUnitValue, ((Number) resolutionUnit.getValue()).intValue()); + + Entry xResolution = ifds.getEntryById(TIFF.TAG_X_RESOLUTION); + assertNotNull(xResolution); + assertEquals(expectedResolutionValue, xResolution.getValue()); + + Entry yResolution = ifds.getEntryById(TIFF.TAG_Y_RESOLUTION); + assertNotNull(yResolution); + assertEquals(expectedResolutionValue, yResolution.getValue()); + } + + @Test + public void testWriteWithCustomSoftwareStandard() throws IOException { + String softwareString = "12M TIFF Test 1.0 (build $foo$)"; + + RenderedImage image = getTestData(0); + + ImageWriter writer = createImageWriter(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + + String standardFormat = IIOMetadataFormatImpl.standardMetadataFormatName; + IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), null); + + IIOMetadataNode customMeta = new IIOMetadataNode(standardFormat); + + IIOMetadataNode dimension = new IIOMetadataNode("Text"); + customMeta.appendChild(dimension); + + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + dimension.appendChild(textEntry); + textEntry.setAttribute("keyword", "Software"); + textEntry.setAttribute("value", softwareString); + + metadata.mergeTree(standardFormat, customMeta); + + writer.write(null, new IIOImage(image, null, metadata), null); + } + catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + assertTrue("No image data written", buffer.size() > 0); + + Directory ifds = new EXIFReader().read(new ByteArrayImageInputStream(buffer.toByteArray())); + Entry software = ifds.getEntryById(TIFF.TAG_SOFTWARE); + assertNotNull(software); + assertEquals(softwareString, software.getValueAsString()); } }