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.
Thanks for posting! Very helpful!
ReplyDeleteExcellent article. When I printed the metadata from your example, I got the following. I see that the inserted keywords occur two times (twice, as you say). Is this as intended? Or, am I inserting two times or just printing two time incorrectly?
ReplyDeleteFormat name: javax_imageio_png_1.0
Format name: javax_imageio_1.0
This is the code I am using to print:
File file = new File(fileName);
ImageInputStream iis = ImageIO.createImageInputStream(file);
if (iis == null)
{
System.out.printf("Unable to Open File: %s\n", fileName);
return;
}
else
{
Iterator readers = ImageIO.getImageReaders(iis);
if (readers.hasNext())
{
// pick the first available ImageReader
ImageReader reader = readers.next();
// attach source to the reader
reader.setInput(iis, true);
// read metadata of first image
IIOMetadata metadata = reader.getImageMetadata(0);
String[] names = metadata.getMetadataFormatNames();
int length = names.length;
for (int i = 0; i < length; i++)
{
System.out.println("Format name: " + names[i]);
displayMetadata(metadata.getAsTree(names[i]));
}
}
Right I've looked into this and I think I know what you are seeing. You will find the the title/author (or whatever keywords you added) appear under both the PNG specific javax_imageio_png_1.0 section and the generic javax_imageio_1.0. This is entirely normal. The first section shows what was encoded in the actual image file, while the second is Java's generic metadata format and is generated (in part) from the PNG specific stuff.
Delete