I've written a number of applications for manipulating photos and most produce PNG files as output. I output PNG instead of JPEG for two reasons; support for transparency and lossless compression. The downside to using PNG files is that there is no default support for adding metadata, such as EXIF information, to the images. In most situations this isn't a problem, but I decided it would be nice to allow the user to add a title or copyright information to the images. Now PNG files don't support EXIF but they do support textual key-value pairs.
Section 11.3.4 of the
PNG Specification details the support within the file format for textual information. Text is stored within PNG files as key-value pairs and the specification gives the following list of default keywords:
Title | Short (one line) title or caption for image |
Author | Name of image's creator |
Description | Description of image (possibly long) |
Copyright | Copyright notice |
Creation Time | Time of original image creation |
Software | Software used to create the image |
Disclaimer | Legal disclaimer |
Warning | Warning of nature of content |
Source | Device used to create the image |
Comment | Miscellaneous comment |
Whilst it makes sense to stick with these keywords (so other software can make use of the information) the specification also states that
other keywords may be defined for other purposes. Currently I can't think of any information that I want to add to PNG files that isn't covered by the default keyword list, all I needed to do was figure out how to actually add the information.
I assumed that I'd be able to quickly find some code on the Internet for doing this kind of thing. Unfortunately it turns out that there are plenty of web sites that describe in detail how to add/retrieve metadata from JPEG images (including EXIF and IPTC), but I couldn't find a single useful example of adding information to PNG files and so I had to figure it out for myself. The applications I wanted to add this feature to are all written in Java and so I headed to the documentation to see what I could find.
I was writing PNG files using the static convenience methods of
javax.ImageIO which don't allow for much customization; you pass an image, a file handle and the format name and it uses default values to write the image to disk. Fortunately you can use the classes directly and have a lot more control over the processing, including altering any associated metadata.
The ImageIO package uses an XML tree structure to represent metadata and there is a DTD describing the supported metadata for each image format. The DTD describing
PNG metadata includes the elements for storing textual information and it was fairly straightforward to write code to add new elements to the structure.
While testing the code I noticed that as well as the native PNG metadata there was also support for a
plugin neutral metadata format. I converted my code to use this format instead and got the same results as before. So why, you ask, would I want to do this?
If you use the neutral metadata format then the image writers convert this into their own metadata format when writing out the image. This means that I could specify, for example, the title of the image and it would appear in a PNG file but it would also get converted into a JPEG header comment if I switched output formats. There is no guarantee that information in the standard metadata format will be preserved by the different plugins so you need to experiment a little (for example if you specify multiple text elements only one of them gets retained as the JPEG comment element).
So without further ado here is the method I wrote to save a PNG file with embedded keywords.
public static void writeImage(RenderedImage image,
Map<String, String> keywords, File file) throws IOException {
ImageWriter writer = null;
OutputStream out = null;
ImageOutputStream ios = null;
try {
// find a writer for the image format
Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("png");
if (iter.hasNext())
writer = iter.next();
if (writer == null)
throw new IOException("Can't Write PNG Files!");
// get the default writer parameters
ImageWriteParam iwparam = writer.getDefaultWriteParam();
// get the default metadata that we will add to
IIOMetadata metadata = writer.getDefaultImageMetadata(
new ImageTypeSpecifier(image), iwparam);
// if there are keywords then...
if (keywords != null && keywords.size() > 0) {
// if we are not allowed to edit the standard metadata then...
if (metadata.isReadOnly()
|| !metadata.isStandardMetadataFormatSupported())
throw new IOException("Metadata Cannot Be Edited!");
// create a "Text" node to hold the keywords
IIOMetadataNode text = new IIOMetadataNode("Text");
for (Map.Entry<String, String> keyword : keywords.entrySet()) {
// copy each keyword/value pair into a node
IIOMetadataNode node = new IIOMetadataNode("TextEntry");
node.setAttribute("keyword", keyword.getKey());
node.setAttribute("value", keyword.getValue());
// PNG files only support Latin-1 characters
// hence the value for the encoding attribute
node.setAttribute("encoding", "ISO-8859-1");
// the spec seems to say that we don't need to specify
// these but if you don't you get an exception
node.setAttribute("language", "en");
node.setAttribute("compression", "none");
// add the keyword node to the "Text" node
text.appendChild(node);
}
// the text node has to be in the right place in the
// tree before we can merge it
IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
root.appendChild(text);
// merge the keywords into the existing metadata
metadata.mergeTree("javax_imageio_1.0", root);
}
// setup the writers ready
out = new FileOutputStream(file);
ios = ImageIO.createImageOutputStream(out);
writer.setOutput(ios);
// write out the image with it's metadata
writer.write(null, new IIOImage(image, null, metadata), iwparam);
ios.flush();
out.flush();
} finally {
// properly close all the writers
if (writer != null)
writer.dispose();
if (ios != null)
ios.close();
if (out != null)
out.close();
}
}
You can also
download a fully working example if you would prefer.