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 Part 2, we looked at how one goes about retrieving metadata from an image as it is read. In this third part, we will do the converse – look at how one goes about setting and writing metadata for a file.

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

Frequently, one wants to include some metadata when using ImageIO to write a file. The most common metadata required (in my experience) is resolution, but there are a wide variety of other information that may need to be included.

The basic process for setting any metadata on an image is as follows:

  1. Create an appropriate XML tree containing the values you wish to set.
  2. Locate an ImageWriter that supports writing the metadata format you need.
  3. Retrieve the default metadata that ImageIO will use when writing your particular image.
  4. Merge your XML tree into the default metadata.
  5. Pass the metadata into the ImageWriter as part of writing the image.

Here is some code that does this:

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

	private static BufferedImage readImage(File file) throws IOException
	{
		ImageInputStream stream = null;
		BufferedImage image = null;
		try
		{
			stream = ImageIO.createImageInputStream(file);
			Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
			if (readers.hasNext())
			{
				ImageReader reader = readers.next();
				reader.setInput(stream);
				image = reader.read(0);
			}
		}
		finally
		{
			if (stream != null)
			{
				stream.close();
			}
		}

		return image;
	}
	
	private static IIOMetadataNode createResolutionMetadata(double resolutionDPI)
	{
		String pixelSize = Double.toString(25.4 / resolutionDPI);
		
		IIOMetadataNode horizontal = new IIOMetadataNode("HorizontalPixelSize");
		horizontal.setAttribute("value", pixelSize);
		
		IIOMetadataNode vertical = new IIOMetadataNode("VerticalPixelSize");
		vertical.setAttribute("value", pixelSize);
		
		IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
		dimension.appendChild(horizontal);
		dimension.appendChild(vertical);
		
		IIOMetadataNode root = new IIOMetadataNode(IIOMetadataFormatImpl.standardMetadataFormatName);
		root.appendChild(dimension);
		
		return root;
	}
	
	private static void writeImage(File outputFile, BufferedImage image, IIOMetadataNode newMetadata) throws IOException
	{
		String extension = getFileExtension(outputFile);
		ImageTypeSpecifier imageType = ImageTypeSpecifier.createFromBufferedImageType(image.getType());
		
		ImageOutputStream stream = null;
		try
		{
			Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(extension);
			while(writers.hasNext())
			{
				ImageWriter writer = writers.next();
				ImageWriteParam writeParam = writer.getDefaultWriteParam();
				IIOMetadata imageMetadata = writer.getDefaultImageMetadata(imageType, writeParam);
				if (!imageMetadata.isStandardMetadataFormatSupported())
				{
					continue;
				}
				if (imageMetadata.isReadOnly())
				{
					continue;
				}
				
				imageMetadata.mergeTree(IIOMetadataFormatImpl.standardMetadataFormatName, newMetadata);
				
				IIOImage imageWithMetadata = new IIOImage(image, null, imageMetadata);
				
				stream = ImageIO.createImageOutputStream(outputFile);
				writer.setOutput(stream);
				writer.write(null, imageWithMetadata, writeParam);
			}
		}
		finally
		{
			if (stream != null)
			{
				stream.close();
			}
		}
	}

	public static void main(String[] args)
	{
		if (args.length != 3)
		{
			System.out
				.println("Usage: ChangeImageResolution inputFile newResolutionDPI outputFile");
			return;
		}

		try
		{
			File inputFile = new File(args[0]);
			double resolutionDPI = Double.parseDouble(args[1]);
			File outputFile = new File(args[2]);
			
			BufferedImage image = readImage(inputFile);
			IIOMetadataNode newMetadata = createResolutionMetadata(resolutionDPI);
			writeImage(outputFile, image, newMetadata);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}
}

Reviewing the steps:

  1. Create an appropriate XML tree containing the values you wish to set.
    This is done by createResolutionMetadata. Note that although an XML tree is desired, you need to use IIOMetadataNodes instead of raw Elements when building your tree.
  2. Locate an ImageWriter that supports writing the metadata format you need.
    This is handled in lines 67-77. This program wants an ImageWriter that supports the standard image metadata format. In addition, it is theoretically possible to find an ImageWriter that doesn’t support setting metadata – in this case isReadOnly will return true.
  3. Retrieve the default metadata that ImageIO will use when writing your particular image.
    Line 69 handles this. Note that the default metadata is dependent on the particular type of BufferedImage that we’re using – we saw in Part 2 how images with palettes had different metadata from those without, for example.
  4. Merge your XML tree into the default metadata.
    Line 79.
  5. Pass the metadata into the ImageWriter as part of writing the image.
    To do this, we create an IIOImage that will hold both the BufferedImage and also our metadata. We then pass this, rather than just the BufferedImage, to the ImageWriter.

Unfortunately, if you experiment with this, it doesn’t work with all the file formats. It works fine with TIFF, but not with PNG. The PNG plugin appears to have a bug in it – although the HorizontalPixelSize and VerticalPixelSize are properly returned (in dimensions of millimeters) when a PNG is read, when you try to set the resolution via the standard image metadata format, the PNG plugin wants the value in pixels per millimeter – the inverse of the “correct” value. If you try this with a BMP file, an exception is thrown inside the BMP plugin.

Chalk up a failure for “common approaches to doing format-specific things.” That being said, this still does show the basic approach. It’s perfectly possible to attack this in a format-specific manner using the format-specific “native” image format, however – you simply build the native image format’s XML tree and merge that tree into that format instead of the “standard” one.

This completes the bulk of this tutorial. In Part 4, we’ll examine a little-used (at least in my experience) feature of IIOMetadata – the ability to programmatically examine the XML structure of the metadata.

 

IIOMetadata Tutorial – Part 3 – Writing Metadata originally appeared on www.silverbaytech.com/blog/.