In Part 1 of this tutorial, we went over some background related to the Java ImageIO
subsystem and how one could retrieve information about the supported ImageReader
s and ImageWriter
s. In Part 2, we looked at how one goes about retrieving metadata from an image as it is read. In Part 3, we looked at writing metadata into a file. In this final part, we will look at how one can (potentially) automatically retrieve information about the format of the metadata.
All the code associated with this tutorial is available at GitHub: https://github.com/SilverBayTech/imageIoMetadata.
Handling the standard image metadata format is pretty straightforward – the DTD for the XML has been published, and it’s the same for any image format that supports it. Suppose, however, that you were trying to write a generic image handling program with a GUI, and wanted to offer the user the ability to fiddle with plugins’ native image metadata. Further, suppose that you wanted this to work regardless of the plugin involved. In this case, you’d need a way of asking the plugin for the data that it supports, potential values for the various settings, etc.
In theory, ImageIO
provides you a way to do this via the IIOMetadataFormat
class. The steps are as follows:
- Locate the
ImageReaderSpi
orImageWriterSpi
for the plugin in question. - Retrieve the
IIOMetadataFormat
object for the native image format or native stream format. - Use the methods supported by
IIOMetadataFormat
to interrogate the various XML elements, their children and their attributes. - Dynamically build a GUI that reflects these values.
I say “in theory,” because this depends on the accuracy and completeness of the information that is returned by this interface.
Although we won’t be building a GUI, the GitHub project includes a program named DumpMetadataFormat
that exercises this interface and allows you to see the information provided for any supported graphics format.
public class DumpMetadataFormat { private static Set<String> ALREADY_DUMPED; private static void indent(int level) { for (int i = 0; i < level; i++) { System.out.print(" "); } } private static void dumpEnumeration(IIOMetadataFormat format, String elementName, String attributeName) { String[] values = format.getAttributeEnumerations(elementName, attributeName); System.out.print(":"); System.out.print(values[0]); for (int i = 1; i < values.length; i++) { System.out.print("|"); System.out.print(values[i]); } } private static void dumpRange(IIOMetadataFormat format, String elementName, String attributeName, int valueType) { String minValue = format.getAttributeMinValue(elementName, attributeName); String maxValue = format.getAttributeMaxValue(elementName, attributeName); System.out.print(":"); System.out.print(minValue); if ((valueType & IIOMetadataFormat.VALUE_RANGE_MIN_INCLUSIVE_MASK) != 0) { System.out.print("<="); } else { System.out.print("<"); } System.out.print("x"); if ((valueType & IIOMetadataFormat.VALUE_RANGE_MAX_INCLUSIVE_MASK) != 0) { System.out.print("<="); } else { System.out.print("<"); } System.out.print(maxValue); } private static void dumpList(IIOMetadataFormat format, String elementName, String attributeName) { int minLength = format.getAttributeListMinLength(elementName, attributeName); int maxLength = format.getAttributeListMaxLength(elementName, attributeName); System.out.print("["); System.out.print(minLength); System.out.print(","); System.out.print(maxLength); System.out.print("]"); } private static void dumpAttributes(IIOMetadataFormat format, String elementName) { String[] attributeNames = format.getAttributeNames(elementName); if (attributeNames != null && attributeNames.length > 0) { for (String attributeName : attributeNames) { System.out.print(" "); System.out.print(attributeName); System.out.print("='"); int dataType = format.getAttributeDataType(elementName, attributeName); switch(dataType) { case IIOMetadataFormat.DATATYPE_BOOLEAN: System.out.print("(BOOLEAN)"); break; case IIOMetadataFormat.DATATYPE_DOUBLE: System.out.print("(DOUBLE)"); break; case IIOMetadataFormat.DATATYPE_FLOAT: System.out.print("(FLOAT)"); break; case IIOMetadataFormat.DATATYPE_INTEGER: System.out.print("(INTEGER)"); break; case IIOMetadataFormat.DATATYPE_STRING: System.out.print("(STRING)"); break; } int valueType = format.getAttributeValueType(elementName, attributeName); switch(valueType) { case IIOMetadataFormat.VALUE_ARBITRARY: case IIOMetadataFormat.VALUE_NONE: break; case IIOMetadataFormat.VALUE_ENUMERATION: dumpEnumeration(format, elementName, attributeName); break; case IIOMetadataFormat.VALUE_LIST: dumpList(format, elementName, attributeName); break; case IIOMetadataFormat.VALUE_RANGE: case IIOMetadataFormat.VALUE_RANGE_MAX_INCLUSIVE: case IIOMetadataFormat.VALUE_RANGE_MIN_INCLUSIVE: case IIOMetadataFormat.VALUE_RANGE_MIN_MAX_INCLUSIVE: dumpRange(format, elementName, attributeName, valueType); break; } String defaultValue = format.getAttributeDefaultValue(elementName, attributeName); if (defaultValue != null) { System.out.print("="); System.out.print(defaultValue); } System.out.print("'"); if (format.isAttributeRequired(elementName, attributeName)) { System.out.print("*"); } } } } private static void dumpElement(IIOMetadataFormat format, String elementName, int indent) { indent(indent); System.out.print("<"); System.out.print(elementName); /* * Handle possible recursion in element children. (TIFF does this) */ if (ALREADY_DUMPED.contains(elementName)) { System.out.println("> (see above)"); return; } ALREADY_DUMPED.add(elementName); dumpAttributes(format, elementName); String[] children = format.getChildNames(elementName); if (children == null || children.length == 0) { System.out.println("/>"); return; } System.out.print("> "); int childPolicy = format.getChildPolicy(elementName); switch(childPolicy) { case IIOMetadataFormat.CHILD_POLICY_ALL: System.out.println("(single instance of all children required)"); break; case IIOMetadataFormat.CHILD_POLICY_CHOICE: System.out.println("(0 or 1 instance of legal child elements)"); break; case IIOMetadataFormat.CHILD_POLICY_EMPTY: System.out.println(""); break; case IIOMetadataFormat.CHILD_POLICY_REPEAT: System.out.println("(zero or more instances of child element)"); break; case IIOMetadataFormat.CHILD_POLICY_SEQUENCE: System.out.println("(sequence of instances of any of its legal child elements)"); break; case IIOMetadataFormat.CHILD_POLICY_SOME: System.out.println("(zero or one instance of each of its legal child elements, in order)"); break; } for (String child : children) { dumpElement(format, child, indent + 1); } indent(indent); System.out.print("</"); System.out.print(elementName); System.out.println(">"); ALREADY_DUMPED.remove(elementName); } private static void dumpFormat(IIOMetadataFormat format) { ALREADY_DUMPED = new HashSet<String>(); String rootNodeName = format.getRootName(); dumpElement(format, rootNodeName, 3); } private static void dumpProvider(ImageReaderSpi provider) { indent(1); System.out.println(provider.getPluginClassName()); if (provider.isStandardImageMetadataFormatSupported()) { indent(2); System.out.print("Image format: "); System.out.println(IIOMetadataFormatImpl.standardMetadataFormatName); IIOMetadataFormat format = provider.getImageMetadataFormat(IIOMetadataFormatImpl.standardMetadataFormatName); dumpFormat(format); } String formatName = provider.getNativeImageMetadataFormatName(); if (formatName != null) { indent(2); System.out.print("Image format: "); System.out.println(formatName); IIOMetadataFormat format = provider.getImageMetadataFormat(formatName); dumpFormat(format); } if (provider.isStandardStreamMetadataFormatSupported()) { indent(2); System.out.print("Stream format: "); System.out.println(IIOMetadataFormatImpl.standardMetadataFormatName); IIOMetadataFormat format = provider.getStreamMetadataFormat(IIOMetadataFormatImpl.standardMetadataFormatName); dumpFormat(format); } formatName = provider.getNativeStreamMetadataFormatName(); if (formatName != null) { indent(2); System.out.print("Stream format: "); System.out.println(formatName); IIOMetadataFormat format = provider.getStreamMetadataFormat(formatName); dumpFormat(format); } } private static boolean supportsFormat(ImageReaderSpi provider, String format) { String[] formats = provider.getFormatNames(); for (String supportedFormat : formats) { if (supportedFormat.equalsIgnoreCase(format)) { return true; } } return false; } private static void dumpFormat(String format) { System.out.print("Format: "); System.out.println(format); IIORegistry registry = IIORegistry.getDefaultInstance(); Iterator<ImageReaderSpi> providers = registry.getServiceProviders(ImageReaderSpi.class, true); while(providers.hasNext()) { ImageReaderSpi provider = providers.next(); if (supportsFormat(provider, format)) { dumpProvider(provider); } } } public static void main(String[] args) { if (args.length == 0) { System.out.println("Usage: DumpMetadataFormat graphicsFormat [...graphicsFormat]"); return; } for (int i = 0; i < args.length; i++) { dumpFormat(args[i]); } System.out.println("Done"); } }
dumpProvider
, starting at line 204, extracts the IIOMetadataFormat
objects (if they exist) for the standard and native image and stream formats. dumpFormat
, on line 197, extracts the root XML element name, and then starts a recursive element-by-element dump procedure by calling dumpElement
.
Each element in the XML has the following:
- A possibly-empty list of child element names, which may be retrieved via
IIOMetadataFormat.getChildNames
- A “child policy,” which may be retrieved via
IIOMetadataFormat.getChildPolicy
There are a set ofCHILD_POLICY_*
constants that describe whether:- All of the children of this element must be present
- Child elements are optional
- Exactly one of the listed child elements must be present
- etc.
This is similar to the information that a DTD might provide, however the choices here are less flexible than the full suite that could be represented in a DTD.
- A possibly-empty list of attribute names, which may be retrieved via
IIOMetadataFormat.getAttributeNames
Each attribute that is defined has the following information:
- An indication as to whether or not this attribute is required
- A data type – boolean, integer, string, etc.
- A value type – this provides information on what the legal values are for this attribute.
- For attributes that are not required, a possibly-
null
default value, indicating the value if this attribute is omitted.
Value types are one of the following:
VALUE_* | Meaning |
---|---|
VALUE_ARBITRARY | There is essentially no restriction on the nature of the value. |
VALUE_ENUM | There is a specific set of legal values. This set may be retrieved using IIOMetadataFormat.getAttributeEnumerations |
VALUE_RANGE | There are a range of legal values. This typically applies to numeric values – integers, floats and doubles. The minimum and maximum values may be obtained using IIOMetadataFormat.getAttributeMinValue and IIOMetadataFormat.getAttributeMaxValue . Ranges may either include or exclude their upper and lower bounds – whether or not the bounds are “inclusive” or “exclusive” are represented by a set of VALUE_RANGE_* values. |
VALUE_LIST | This indicates that there may be more than one value in the attribute, typically separated by spaces.IIOMetadataFormat.getAttributeListMinLength and IIOMetadataFormat.getAttributeListMaxLength allow you to determine the minimum and maximum number of values in the list. |
As an example, here is the result if the program is run for the “PNG” format:
Format: png com.sun.imageio.plugins.png.PNGImageReader Image format: javax_imageio_1.0 <javax_imageio_1.0> (zero or one instance of each of its legal child elements, in order) <Chroma> (zero or one instance of each of its legal child elements, in order) <ColorSpaceType name='(STRING):XYZ|Lab|Luv|YCbCr|Yxy|YCCK|PhotoYCC|RGB|GRAY|HSV|HLS|CMYK|CMY|2CLR|3CLR|4CLR|5CLR|6CLR|7CLR|8CLR|9CLR|ACLR|BCLR|CCLR|DCLR|ECLR|FCLR'*/> <NumChannels value='(INTEGER)[0,2147483647]'*/> <Gamma value='(FLOAT)'*/> <BlackIsZero value='(BOOLEAN):TRUE|FALSE=TRUE'*/> <Palette> (zero or more instances of child element) <PaletteEntry index='(INTEGER)'* red='(INTEGER)'* green='(INTEGER)'* blue='(INTEGER)'* alpha='(INTEGER)=255'/> </Palette> <BackgroundIndex value='(INTEGER)'*/> <BackgroundColor red='(INTEGER)'* green='(INTEGER)'* blue='(INTEGER)'*/> </Chroma> <Compression> (zero or one instance of each of its legal child elements, in order) <CompressionTypeName value='(STRING)'*/> <Lossless value='(BOOLEAN):TRUE|FALSE=TRUE'*/> <NumProgressiveScans value='(INTEGER)'*/> <BitRate value='(FLOAT)'*/> </Compression> <Data> (zero or one instance of each of its legal child elements, in order) <PlanarConfiguration value='(STRING):PixelInterleaved|PlaneInterleaved|LineInterleaved|TileInterleaved'*/> <SampleFormat value='(STRING):SignedIntegral|UnsignedIntegral|Real|Index'*/> <BitsPerSample value='(INTEGER)[1,2147483647]'*/> <SignificantBitsPerSample value='(INTEGER)[1,2147483647]'*/> <SampleMSB value='(INTEGER)[1,2147483647]'*/> </Data> <Dimension> (zero or one instance of each of its legal child elements, in order) <PixelAspectRatio value='(FLOAT)'*/> <ImageOrientation value='(STRING):Normal|Rotate90|Rotate180|Rotate270|FlipH|FlipV|FlipHRotate90|FlipVRotate90'*/> <HorizontalPixelSize value='(FLOAT)'*/> <VerticalPixelSize value='(FLOAT)'*/> <HorizontalPhysicalPixelSpacing value='(FLOAT)'*/> <VerticalPhysicalPixelSpacing value='(FLOAT)'*/> <HorizontalPosition value='(FLOAT)'*/> <VerticalPosition value='(FLOAT)'*/> <HorizontalPixelOffset value='(INTEGER)'*/> <VerticalPixelOffset value='(INTEGER)'*/> <HorizontalScreenSize value='(INTEGER)'*/> <VerticalScreenSize value='(INTEGER)'*/> </Dimension> <Document> (zero or one instance of each of its legal child elements, in order) <FormatVersion value='(STRING)'*/> <SubimageInterpretation value='(STRING):Standalone|SinglePage|FullResolution|ReducedResolution|PyramidLayer|Preview|VolumeSlice|ObjectView|Panorama|AnimationFrame|TransparencyMask|CompositingLayer|SpectralSlice|Unknown'*/> <ImageCreationTime year='(INTEGER)'* month='(INTEGER):1<=x<=12'* day='(INTEGER):1<=x<=31'* hour='(INTEGER):0<=x<=23=0' minute='(INTEGER):0<=x<=59=0' second='(INTEGER):0<=x<=60=0'/> <ImageModificationTime year='(INTEGER)'* month='(INTEGER):1<=x<=12'* day='(INTEGER):1<=x<=31'* hour='(INTEGER):0<=x<=23=0' minute='(INTEGER):0<=x<=59=0' second='(INTEGER):0<=x<=60=0'/> </Document> <Text> (zero or more instances of child element) <TextEntry keyword='(STRING)' value='(STRING)'* language='(STRING)' encoding='(STRING)' compression='(STRING):none|lzw|zip|bzip|other=none'/> </Text> <Transparency> (zero or one instance of each of its legal child elements, in order) <Alpha value='(STRING):none|premultiplied|nonpremultiplied=none'/> <TransparentIndex value='(INTEGER)'*/> <TransparentColor value='(INTEGER)[0,2147483647]'*/> <TileTransparencies> (zero or more instances of child element) <TransparentTile x='(INTEGER)'* y='(INTEGER)'*/> </TileTransparencies> <TileOpacities> (zero or more instances of child element) <OpaqueTile x='(INTEGER)'* y='(INTEGER)'*/> </TileOpacities> </Transparency> </javax_imageio_1.0> Image format: javax_imageio_png_1.0 <javax_imageio_png_1.0> (zero or one instance of each of its legal child elements, in order) <IHDR width='(INTEGER):1<=x<=2147483647'* height='(INTEGER):1<=x<=2147483647'* bitDepth='(INTEGER):1|2|4|8|16'* colorType='(STRING):Grayscale|RGB|Palette|GrayAlpha|RGBAlpha'* compressionMethod='(STRING):deflate'* filterMethod='(STRING):adaptive'* interlaceMethod='(STRING):none|adam7'*/> <PLTE> (zero or more instances of child element) <PLTEEntry index='(INTEGER):0<=x<=255'* red='(INTEGER):0<=x<=255'* green='(INTEGER):0<=x<=255'* blue='(INTEGER):0<=x<=255'*/> </PLTE> <bKGD> (0 or 1 instance of legal child elements) <bKGD_Grayscale gray='(INTEGER):0<=x<=65535'*/> <bKGD_RGB red='(INTEGER):0<=x<=65535'* green='(INTEGER):0<=x<=65535'* blue='(INTEGER):0<=x<=65535'*/> <bKGD_Palette index='(INTEGER):0<=x<=255'*/> </bKGD> <cHRM whitePointX='(INTEGER):0<=x<=65535'* whitePointY='(INTEGER):0<=x<=65535'* redX='(INTEGER):0<=x<=65535'* redY='(INTEGER):0<=x<=65535'* greenX='(INTEGER):0<=x<=65535'* greenY='(INTEGER):0<=x<=65535'* blueX='(INTEGER):0<=x<=65535'* blueY='(INTEGER):0<=x<=65535'*/> <gAMA value='(INTEGER):0<=x<=2147483647'*/> <hIST> (zero or more instances of child element) <hISTEntry index='(INTEGER):0<=x<=255'* value='(INTEGER):0<=x<=65535'*/> </hIST> <iCCP profileName='(STRING)'* compressionMethod='(STRING):deflate'*/> <iTXt> (zero or more instances of child element) <iTXtEntry keyword='(STRING)'* compressionFlag='(BOOLEAN):TRUE|FALSE'* compressionMethod='(STRING)'* languageTag='(STRING)'* translatedKeyword='(STRING)'* text='(STRING)'*/> </iTXt> <pHYS pixelsPerUnitXAxis='(INTEGER):0<=x<=2147483647'* pixelsPerUnitYAxis='(INTEGER):0<=x<=2147483647'* unitSpecifier='(STRING):unknown|meter'*/> <sBIT> (0 or 1 instance of legal child elements) <sBIT_Grayscale gray='(INTEGER):0<=x<=255'*/> <sBIT_GrayAlpha gray='(INTEGER):0<=x<=255'* alpha='(INTEGER):0<=x<=255'*/> <sBIT_RGB red='(INTEGER):0<=x<=255'* green='(INTEGER):0<=x<=255'* blue='(INTEGER):0<=x<=255'*/> <sBIT_RGBAlpha red='(INTEGER):0<=x<=255'* green='(INTEGER):0<=x<=255'* blue='(INTEGER):0<=x<=255'* alpha='(INTEGER):0<=x<=255'*/> <sBIT_Palette red='(INTEGER):0<=x<=255'* green='(INTEGER):0<=x<=255'* blue='(INTEGER):0<=x<=255'*/> </sBIT> <sPLT> (zero or more instances of child element) <sPLTEntry index='(INTEGER):0<=x<=255'* red='(INTEGER):0<=x<=255'* green='(INTEGER):0<=x<=255'* blue='(INTEGER):0<=x<=255'* alpha='(INTEGER):0<=x<=255'*/> </sPLT> <sRGB renderingIntent='(STRING):Perceptual|Relative colorimetric|Saturation|Absolute colorimetric'*/> <tEXt> (zero or more instances of child element) <tEXtEntry keyword='(STRING)'* value='(STRING)'*/> </tEXt> <tIME year='(INTEGER):0<=x<=65535'* month='(INTEGER):1<=x<=12'* day='(INTEGER):1<=x<=31'* hour='(INTEGER):0<=x<=23'* minute='(INTEGER):0<=x<=59'* second='(INTEGER):0<=x<=60'*/> <tRNS> (0 or 1 instance of legal child elements) <tRNS_Grayscale gray='(INTEGER):0<=x<=65535'*/> <tRNS_RGB red='(INTEGER):0<=x<=65535'* green='(INTEGER):0<=x<=65535'* blue='(INTEGER):0<=x<=65535'*/> <tRNS_Palette index='(INTEGER):0<=x<=255'* alpha='(INTEGER):0<=x<=255'*/> </tRNS> <zTXt> (zero or more instances of child element) <zTXtEntry keyword='(STRING)'* compressionMethod='(STRING):deflate'* text='(STRING)'*/> </zTXt> <UnknownChunks> (zero or more instances of child element) <UnknownChunk type='(STRING)'*/> </UnknownChunks> </javax_imageio_png_1.0> Done
If you look at this, you will see that:
javax_imageio_1.0/Chroma
allows either zero or one of each of its child elements. Thus, for example, theColorSpaceType
element is not required, but there cannot be two of them.javax_imageio_1.0/Chroma/ColorSpaceType
has an attributename
which is required, and is a enumeration.javax_imageio_1.0/Chroma/Palette
allows more than one childPaletteEntry
instance, as one would expect.Palette
is not required, however – it would not be present on a non-indexed image.PaletteEntry
requiresindex
,red
,green
andblue
attributes, however thealpha
attribute is optional, defaulting to 255 if not present.- The attributes for
javax_imageio_1.0/Document/ImageCreationTime
andjavax_imageio_1.0/Document/ImageModificationTime
include ranges that restrict the values to legal month, day, year, hour, minute and second values.
Here is the output from DumpMetadataFormat
for a PNG image, allowing you to compare actual data with the “schema” described above:
Reader: com.sun.imageio.plugins.png.PNGImageReader Image metadata Format name: javax_imageio_png_1.0 <javax_imageio_png_1.0> <IHDR width='157' height='56' bitDepth='8' colorType='RGB' compressionMethod='deflate' filterMethod='adaptive' interlaceMethod='none'/> <gAMA value='45455'/> <pHYs pixelsPerUnitXAxis='11811' pixelsPerUnitYAxis='11811' unitSpecifier='meter'/> <tIME year='2014' month='5' day='14' hour='16' minute='31' second='22'/> </javax_imageio_png_1.0> Format name: javax_imageio_1.0 <javax_imageio_1.0> <Chroma> <ColorSpaceType name='RGB'/> <NumChannels value='3'/> <Gamma value='0.45455'/> <BlackIsZero value='TRUE'/> </Chroma> <Compression> <CompressionTypeName value='deflate'/> <Lossless value='TRUE'/> <NumProgressiveScans value='1'/> </Compression> <Data> <PlanarConfiguration value='PixelInterleaved'/> <SampleFormat value='UnsignedIntegral'/> <BitsPerSample value='8 8 8'/> </Data> <Dimension> <PixelAspectRatio value='1.0'/> <ImageOrientation value='Normal'/> <HorizontalPixelSize value='0.08466683'/> <VerticalPixelSize value='0.08466683'/> </Dimension> <Document> <ImageModificationTime year='2014' month='5' day='14' hour='16' minute='31' second='22'/> </Document> <Transparency> <Alpha value='none'/> </Transparency> </javax_imageio_1.0> Done
Unfortunately, it has been my experience that the “list” information is largely useless – every instance I have seen specifies “between 1 and 2 billion entries in the list,” despite the fact that PNG files, for example, shouldn’t have more than four components (red, green, blue and alpha). Similarly, when we dump the data for the TIFF file format, the plugin that is provided by the JAI ImageIO jar has a recursion – the TIFFIFD
element is defined to potentially contain another TIFFIFD
element, which strikes me as odd.
Finally, it is not clear that even if one could build a GUI to support all the settings offered by a file format, it is not clear that one should. Take the PNG file format, for example. The metadata formats provide two different ways of specifying the palette – one via the standard image format, and one via the PNG-specific native format. What would happen if you set them differently, or if they didn’t correspond to the BufferedImage
that you were writing? What would happen if you tried to provide palette metadata on a non-indexed image? As soon as you start dealing with exceptions like this, you are into format-specific code, at which point (if you’re going to be writing images out) it’s probably better to work with the DTD’s rather than with the enumerated format. It’s possible that you could use this information in some way for display-only purposes, but again I wonder…
Thus, although it is technically possible to extract format information, I have to chalk this up to “a nice idea, but not as useful as it initially sounds.” But it’s still interesting to have explored it a bit.
IIOMetadata Tutorial – Part 4 – Automatically Determining Metadata Format originally appeared on www.silverbaytech.com/blog/.