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 ImageReaders and ImageWriters. In this second part, we will look at how one goes about retrieving metadata from an image as it is read.

All the code associated with this tutorial is available at GitHub: https://github.com/SilverBayTech/imageIoMetadata.

Let us suppose that we have an image file, and have located a particular ImageReader that supports it. We can connect up our ImageReader as follows:

ImageInputStream stream = ImageIO.createImageInputStream(file);
reader.setInput(stream, true);

At this point, the reader has access to the data from the file. As a result, it is possible for us to now obtain either image metadata or stream metadata using one of the following two calls:

IIOMetadata imageMetadata = reader.getImageMetadata(0);
IIOMetadata streamMetadata = reader.getStreamMetadata();

getImageMetadata takes an int argument because some file formats – TIFF and GIF among them – support more than one image within a particular file. Either method may return null if no such metadata is available. All the standard and JAI ImageIO plugins support at least some form of image metadata, although many do not support stream metadata.

If you look at the IIOMetadata class, however, you will see that it doesn’t appear to have much in the way of methods to retrieve data. The reason for this is that the ImageIO system represents the actual metadata in the form of an XML tree. To obtain the actual metadata, one must call the getAsTree method on the IIOMetadata object, passing in one of the supported metadata format names. One can determine these format names through the ImageReaderSpi instance as described in the previous part of this tutorial, or the IIOMetadata object provides a getMetadataFormatNames method that will return an array containing all the supported formats.

The return value from getAsTree is an org.w3c.dom.Node that represents the root of an XML tree in the particular format. Once this Node has been obtained, one can use the standard DOM methods to walk through the tree as required. I have provided a DumpImageMetadata program that does just that:

public class DumpImageMetadata
{
	private static String getFileExtension(File file)
	{
		String fileName = file.getName();
		int lastDot = fileName.lastIndexOf('.');
		return fileName.substring(lastDot + 1);
	}

	private static void indent(int level)
	{
		for (int i = 0; i < level; i++)
		{
			System.out.print("    ");
		}
	}

	private static void displayAttributes(NamedNodeMap attributes)
	{
		if (attributes != null)
		{
			int count = attributes.getLength();
			for (int i = 0; i < count; i++)
			{
				Node attribute = attributes.item(i);

				System.out.print(" ");
				System.out.print(attribute.getNodeName());
				System.out.print("='");
				System.out.print(attribute.getNodeValue());
				System.out.print("'");
			}
		}
	}

	private static void displayMetadataNode(Node node, int level)
	{
		indent(level);
		System.out.print("<");
		System.out.print(node.getNodeName());

		NamedNodeMap attributes = node.getAttributes();
		displayAttributes(attributes);

		Node child = node.getFirstChild();
		if (child == null)
		{
			String value = node.getNodeValue();
			if (value == null || value.length() == 0)
			{
				System.out.println("/>");
			}
			else
			{
				System.out.print(">");
				System.out.print(value);
				System.out.print("<");
				System.out.print(node.getNodeName());
				System.out.println(">");
			}
			return;
		}

		System.out.println(">");
		while (child != null)
		{
			displayMetadataNode(child, level + 1);
			child = child.getNextSibling();
		}

		indent(level);
		System.out.print("</");
		System.out.print(node.getNodeName());
		System.out.println(">");
	}
	
	private static void dumpMetadata(IIOMetadata metadata)
	{
		String[] names = metadata.getMetadataFormatNames();
		int length = names.length;
		for (int i = 0; i < length; i++)
		{
			indent(2);
			System.out.println("Format name: " + names[i]);
			displayMetadataNode(metadata.getAsTree(names[i]), 3);
		}
	}

	private static void processFileWithReader(File file, ImageReader reader) throws IOException
	{
		ImageInputStream stream = null;

		try
		{
			stream = ImageIO.createImageInputStream(file);

			reader.setInput(stream, true);

			IIOMetadata metadata = reader.getImageMetadata(0);
			
			indent(1);
			System.out.println("Image metadata");
			dumpMetadata(metadata);
			
			metadata = reader.getStreamMetadata();
			if (metadata != null)
			{
				indent(1);
				System.out.println("Stream metadata");
				dumpMetadata(metadata);
			}

		}
		finally
		{
			if (stream != null)
			{
				stream.close();
			}
		}
	}

	private static void processFile(File file) throws IOException
	{
		System.out.println("\nProcessing " + file.getName() + ":\n");

		String extension = getFileExtension(file);

		Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(extension);

		while (readers.hasNext())
		{
			ImageReader reader = readers.next();

			System.out.println("Reader: " + reader.getClass().getName());

			processFileWithReader(file, reader);
		}
	}

	private static void processDirectory(File directory) throws IOException
	{
		System.out.println("Processing all files in " + directory.getAbsolutePath());

		File[] contents = directory.listFiles();
		for (File file : contents)
		{
			if (file.isFile())
			{
				processFile(file);
			}
		}
	}

	public static void main(String[] args)
	{
		try
		{
			for (int i = 0; i < args.length; i++)
			{
				File fileOrDirectory = new File(args[i]);

				if (fileOrDirectory.isFile())
				{
					processFile(fileOrDirectory);
				}
				else
				{
					processDirectory(fileOrDirectory);
				}
			}

			System.out.println("\nDone");
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}
}

The project on GitHub contains a number of sample files in the testFiles directory:

File Format
test.gif GIF file
test.jpeg JPEG file
test8.png 8-bit (paletted) PNG file
test24.png 24-bit PNG file
test4.bmp 4-bit (paletted) BMP file
test8.bmp 8-bit (paletted) BMP file
test24.bmp 24-bit BMP file
test8.tif 8-bit (paletted) TIFF file
test24.tif 24-bit TIFF file

You can execute the DumpImageMetadata program on a single file, a list of files, or on the testFiles directory, in which case it will iterate through all the files in that directory. For each file, the program dumps the XML tree associated with each of the metadata formats it finds.

If you execute the program on the file test24.png for example, you will get the following output:

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>

The format named javax_imageio_1.0 is the “standard” image metadata format. (If you are not into putting random strings like this into your code, you’ll find this defined in the javax.imageio.metadata.IIOMetadataFormatImpl class.) Most of the items in this tree should be self-explanatory. HorizontalPixelSize and VerticalPixelSize are represented in pixels per millimeter. If you do the math, this comes out to 300 DPI which is, indeed, the resolution at which this image was created.

The format named javax_imageio_png_1.0 is the “native” image metadata format for PNG. The names of the child elements may look a little odd, but if you know anything about the internals for the PNG format, you’ll find that they correspond to various “chunk” names defined in the PNG specification.

If you repeat the operation for test8.png, you’ll get the following result:

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='Palette' compressionMethod='deflate' filterMethod='adaptive' interlaceMethod='none'/>
                <PLTE>
                    <PLTEEntry index='0' red='121' green='121' blue='121'/>
                    <PLTEEntry index='1' red='229' green='233' blue='239'/>
                    <PLTEEntry index='2' red='234' green='237' blue='242'/>
                    <PLTEEntry index='3' red='225' green='225' blue='225'/>
                    ...many entries omitted...
                    <PLTEEntry index='255' red='0' green='0' blue='0'/>
                </PLTE>
                <gAMA value='45455'/>
                <pHYs pixelsPerUnitXAxis='11810' pixelsPerUnitYAxis='11810' unitSpecifier='meter'/>
                <tIME year='2014' month='5' day='14' hour='18' minute='52' second='20'/>
            </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'/>
                    <Palette>
                        <PaletteEntry index='0' red='121' green='121' blue='121'/>
                        <PaletteEntry index='1' red='229' green='233' blue='239'/>
                        <PaletteEntry index='2' red='234' green='237' blue='242'/>
                        <PaletteEntry index='3' red='225' green='225' blue='225'/>
                        ...many entries omitted...
                        <PaletteEntry index='255' red='0' green='0' blue='0'/>
                    </Palette>
                </Chroma>
                <Compression>
                    <CompressionTypeName value='deflate'/>
                    <Lossless value='TRUE'/>
                    <NumProgressiveScans value='1'/>
                </Compression>
                <Data>
                    <PlanarConfiguration value='PixelInterleaved'/>
                    <SampleFormat value='Index'/>
                    <BitsPerSample value='8 8 8'/>
                </Data>
                <Dimension>
                    <PixelAspectRatio value='1.0'/>
                    <ImageOrientation value='Normal'/>
                    <HorizontalPixelSize value='0.08467401'/>
                    <VerticalPixelSize value='0.08467401'/>
                </Dimension>
                <Document>
                    <ImageModificationTime year='2014' month='5' day='14' hour='18' minute='52' second='20'/>
                </Document>
                <Transparency>
                    <Alpha value='none'/>
                </Transparency>
            </javax_imageio_1.0>

The significant difference, of course, is that this file is indexed – contains a palette of colors – and thus both the standard and PNG-specific metadata contain palette information.

Neither of the above two files had stream metadata associated with them. TIFF files, however, do. Here’s what happens when you dump the metadata for test24.tif:

Reader: com.sun.media.imageioimpl.plugins.tiff.TIFFImageReader
    Image metadata
        Format name: com_sun_media_imageio_plugins_tiff_image_1.0
            <com_sun_media_imageio_plugins_tiff_image_1.0>
                <TIFFIFD tagSets='com.sun.media.imageio.plugins.tiff.BaselineTIFFTagSet,com.sun.media.imageio.plugins.tiff.FaxTIFFTagSet,com.sun.media.imageio.plugins.tiff.EXIFParentTIFFTagSet,com.sun.media.imageio.plugins.tiff.GeoTIFFTagSet'>
                    <TIFFField number='254' name='NewSubfileType'>
                        <TIFFLongs>
                            <TIFFLong value='0' description='Default'/>
                        </TIFFLongs>
                    </TIFFField>
                    <TIFFField number='256' name='ImageWidth'>
                        <TIFFShorts>
                            <TIFFShort value='157'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='257' name='ImageLength'>
                        <TIFFShorts>
                            <TIFFShort value='56'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='258' name='BitsPerSample'>
                        <TIFFShorts>
                            <TIFFShort value='8'/>
                            <TIFFShort value='8'/>
                            <TIFFShort value='8'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='259' name='Compression'>
                        <TIFFShorts>
                            <TIFFShort value='5' description='LZW'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='262' name='PhotometricInterpretation'>
                        <TIFFShorts>
                            <TIFFShort value='2' description='RGB'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='273' name='StripOffsets'>
                        <TIFFLongs>
                            <TIFFLong value='86'/>
                            <TIFFLong value='1254'/>
                            <TIFFLong value='3137'/>
                            <TIFFLong value='4271'/>
                            <TIFFLong value='5430'/>
                            <TIFFLong value='6845'/>
                            <TIFFLong value='8158'/>
                        </TIFFLongs>
                    </TIFFField>
                    <TIFFField number='277' name='SamplesPerPixel'>
                        <TIFFShorts>
                            <TIFFShort value='3'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='278' name='RowsPerStrip'>
                        <TIFFLongs>
                            <TIFFLong value='8'/>
                        </TIFFLongs>
                    </TIFFField>
                    <TIFFField number='279' name='StripByteCounts'>
                        <TIFFLongs>
                            <TIFFLong value='1168'/>
                            <TIFFLong value='1883'/>
                            <TIFFLong value='1134'/>
                            <TIFFLong value='1159'/>
                            <TIFFLong value='1415'/>
                            <TIFFLong value='1313'/>
                            <TIFFLong value='1083'/>
                        </TIFFLongs>
                    </TIFFField>
                    <TIFFField number='282' name='XResolution'>
                        <TIFFRationals>
                            <TIFFRational value='118/1'/>
                        </TIFFRationals>
                    </TIFFField>
                    <TIFFField number='283' name='YResolution'>
                        <TIFFRationals>
                            <TIFFRational value='118/1'/>
                        </TIFFRationals>
                    </TIFFField>
                    <TIFFField number='284' name='PlanarConfiguration'>
                        <TIFFShorts>
                            <TIFFShort value='1' description='Chunky'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='296' name='ResolutionUnit'>
                        <TIFFShorts>
                            <TIFFShort value='3' description='Centimeter'/>
                        </TIFFShorts>
                    </TIFFField>
                    <TIFFField number='317' name='Predictor'>
                        <TIFFShorts>
                            <TIFFShort value='2' description='Horizontal Differencing'/>
                        </TIFFShorts>
                    </TIFFField>
                </TIFFIFD>
            </com_sun_media_imageio_plugins_tiff_image_1.0>
        Format name: javax_imageio_1.0
            <javax_imageio_1.0>
                <Chroma>
                    <ColorSpaceType name='RGB'/>
                    <BlackIsZero value='TRUE'/>
                    <NumChannels value='3'/>
                </Chroma>
                <Compression>
                    <CompressionTypeName value='LZW'/>
                    <Lossless value='TRUE'/>
                    <NumProgressiveScans value='1'/>
                </Compression>
                <Data>
                    <PlanarConfiguration value='PixelInterleaved'/>
                    <SampleFormat value='UnsignedIntegral'/>
                    <BitsPerSample value='8 8 8'/>
                    <SampleMSB value='7 7 7'/>
                </Data>
                <Dimension>
                    <PixelAspectRatio value='1.0'/>
                    <HorizontalPixelSize value='0.084745765'/>
                    <VerticalPixelSize value='0.084745765'/>
                </Dimension>
                <Document>
                    <FormatVersion value='6.0'/>
                </Document>
                <Transparency>
                    <Alpha value='none'/>
                </Transparency>
            </javax_imageio_1.0>
    Stream metadata
        Format name: com_sun_media_imageio_plugins_tiff_stream_1.0
            <com_sun_media_imageio_plugins_tiff_stream_1.0>
                <ByteOrder value='LITTLE_ENDIAN'/>
            </com_sun_media_imageio_plugins_tiff_stream_1.0>

If you are familiar with the TIFF file format, you will recognize some of the information in the com_sun_media_imageio_plugins_tiff_image_1.0 format image metadata. The com_sun_media_imageio_plugins_tiff_stream_1.0 format stream data, as we discussed in Part 1, provides the byte order information for the file – little endian, in this case.

Assuming that you intend to go diving into the metadata looking for information, how do you know what the XML structure is going to be? For the image formats supported by the core Java 7 plug-ins, the DTD for the XML is included Package overview for the javax.imageio.metadata package in the Java 7 Javadoc:

Documentation for the JAI ImageIO plugins is a little more obscure. You can download the Javadoc at http://download.java.net/media/jai-imageio/builds/release/1.1/jai_imageio-1_1-doc.zip. The metadata formats are then documented in the package overviews for the various plug-in classes – com.sun.media.imageio.plugins.tiff for TIFF, as one example.

As an example, here is a program named GetImageResolution which extracts and parses image metadata and dumps the horizontal and vertical resolution for an image:

public class GetImageResolution
{
	private static final NumberFormat FORMAT = new DecimalFormat("#0.0");

	private static String getFileExtension(File file)
	{
		String fileName = file.getName();
		int lastDot = fileName.lastIndexOf('.');
		return fileName.substring(lastDot + 1);
	}

	private static Element getChildElement(Node parent, String name)
	{
		NodeList children = parent.getChildNodes();
		int count = children.getLength();
		for (int i = 0; i < count; i++)
		{
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE)
			{
				if (child.getNodeName().equals(name))
				{
					return (Element)child;
				}
			}
		}

		return null;
	}

	private static void dumpResolution(String title, Element element)
	{
		System.out.print(title);
		if (element == null)
		{
			System.out.println("(none)");
			return;
		}

		String value = element.getAttribute("value");
		if (value == null)
		{
			System.out.println("(none)");
			return;
		}

		double mmPerPixel = Double.parseDouble(value);
		double pixelsPerInch = 25.4 / mmPerPixel;

		System.out.print(FORMAT.format(pixelsPerInch));
		System.out.println(" pixels per inch");
	}

	private static void processFileWithReader(File file, ImageReader reader) throws IOException
	{
		ImageInputStream stream = null;

		try
		{
			stream = ImageIO.createImageInputStream(file);

			reader.setInput(stream, true);

			IIOMetadata metadata = reader.getImageMetadata(0);

			Node root = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
			Element dimension = getChildElement(root, "Dimension");
			if (dimension != null)
			{
				Element horizontalPixelSize = getChildElement(dimension, "HorizontalPixelSize");
				Element verticalPixelSize = getChildElement(dimension, "VerticalPixelSize");

				dumpResolution("    Horizontal resolution: ", horizontalPixelSize);
				dumpResolution("    Vertical resolution: ", verticalPixelSize);
			}
		}
		finally
		{
			if (stream != null)
			{
				stream.close();
			}
		}
	}

	private static void processFile(File file) throws IOException
	{
		System.out.println("\nProcessing " + file.getName() + ":\n");

		String extension = getFileExtension(file);

		Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(extension);

		while (readers.hasNext())
		{
			ImageReader reader = readers.next();

			ImageReaderSpi spi = reader.getOriginatingProvider();

			if (spi.isStandardImageMetadataFormatSupported())
			{
				processFileWithReader(file, reader);
				return;
			}
		}

		System.out.println("    No compatible reader found");
	}

	private static void processDirectory(File directory) throws IOException
	{
		System.out.println("Processing all files in " + directory.getAbsolutePath());

		File[] contents = directory.listFiles();
		for (File file : contents)
		{
			if (file.isFile())
			{
				processFile(file);
			}
		}
	}

	public static void main(String[] args)
	{
		try
		{
			for (int i = 0; i < args.length; i++)
			{
				File fileOrDirectory = new File(args[i]);

				if (fileOrDirectory.isFile())
				{
					processFile(fileOrDirectory);
				}
				else
				{
					processDirectory(fileOrDirectory);
				}
			}

			System.out.println("\nDone");
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}
}

Of course, if you run it on test.gif, you will see that it prints “(none)” for the resolutions. The GIF file format doesn’t really have the concept of “pixels per unit measure” since it wasn’t intended for printing. As such, the metadata omits the HorizontalPixelSize and VerticalPixelSize elements within the Dimension container element. This is one of the things you have to watch out for – not every image has every piece of metadata.

In Part 3 of this tutorial, we will look at how to set metadata so that it is written as part of a file.

 

IIOMetadata Tutorial – Part 2 – Retrieving Image Metadata originally appeared on the Silver Bay Tech blog.