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 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:
- Overview – http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/package-summary.html
- GIF – http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/gif_metadata.html
- JPEG – http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
- PNG – http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/png_metadata.html
- BMP – http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/bmp_metadata.html
- WBMP – http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/wbmp_metadata.html
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.