PNG Keywords

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.

3 comments:

  1. Thanks for posting! Very helpful!

    ReplyDelete
  2. Excellent 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?

    Format 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]));
    }
    }

    ReplyDelete
    Replies
    1. 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