SVGRoundTrip, Part 3b: Convert SVG to Java 2D

Having re-read the previous post I realised that the stuff on supporting unsupported SVG elements could have been clearer. Somehow I doubt I could write it any better so I thought I'd show an example instead.


The SVG file used in these three examples contains both embedded raster images and text nodes which, by default, are unsupported. If we specify that we wish to skip unsupported SVG elements (-u skip) then what we get is a Java class that will draw the left hand image. If we specify that we want to add image support (-i) then we get the middle image. And finally, if we say that unsupported SVG elements should be treated as images (-u image) then we get the right hand image.

Whilst it may look like the right hand image is best what you have to remember is that only the elements in the left most image will scale without a loss of quality.

SVGRoundTrip, Part 3: Convert SVG to Java 2D

As I mentioned in the first post on SVGRoundTrip, my initial interest in SVG images came from wanting scalable images that I could use within the UI of my applications.

When I originally started work on 3DAssembler I used PNG versions of the icons from the Tango and Gnome Desktop icon sets as I didn't know how to deal with the original SVG versions. During development I noticed that the Flamingo component suite (which I was using for the ribbon) also contained support for converting SVG files into Java 2D based classes that would draw the icons at the correct size we required. For version 2 of 3DAssembler I changed from all PNG images to all SVG and the different sized icons look much better. Whilst most of the SVG files I wanted to used converted without any problems some didn't and so I set about figuring out why.

The first problem that I noticed was that the clipping wasn't correct. This usually showed up where I had an image that went outside the bounds of the SVG page size. When exporting to a PNG from Inkscape everything outside the page boundaries was removed and I expected the same behavior when the images were converted to Java code. It was definitely a bug as the image being drawn in my app was actually a lot wider than it should have been and because it wasn't being clipped was actually spilling out over other components. I initially fixed the issue by simply setting a clip on the Graphics2D object before passing it to the generated Java code. Whilst this fixed the overflow issue it didn't fix clipping issues within the bounds of the image. Each element in an SVG file can specify a clip and this was being ignored when the code within Flamingo was converting the SVG file to Java code. After a little trial and error I managed to add support for clipping any SVG element that required it.

The second problem related to scaling. Whilst SVG images can be scaled at will without a loss in quality they are originally drawn at a specific size. Knowing how to scale all the SVG elements requires knowing this original size. Unfortunately, the code in the Flamingo library didn't extract the page size from the original SVG file rather it used the bounds of the image (i.e. the rectangle that fully encloses all the elements) for scaling. Usually (at least in the files I was using) the bounding rectangle only differed from the page size by a pixel or so and therefore the images drawn using the wrong information were almost identical to those using the correct page size. The problem really only appeared when I fixed the clipping issue. With correct clipping I kept seeing the right/bottom column/row of the image being clipped. It turns out that the correct page size information is available (although it's well buried) in the information provided by Batik when it parses the SVG file so it was fairly easy to fix this problem as well.

The final problem that I noticed was that the code wasn't transcoding alpha values properly either. Each SVG element can have an alpha value associated with it to set the transparency. The problem arose when elements were nested. So for example if you have a set of nested shapes and you set alpha to 0.5 for the root element all the nested elements should have this value to start with before any other values are applied. Again once I knew what the problem was fixing it was easy.

These three fixes allowed me to convert all the SVG files I wanted to use for 3DAssembler without any problems. I patched my copy of Flamingo and then posted the patch to the Flamingo discussion forum. I would probably have left it at that, but a few days later a message was posted by the developer of Flamingo to say that he was no longer supporting or developing the library, and so SVGRoundTrip was born to hold these fixes but also as a place for new features and other SVG related ideas.

The original library (as well as having the bugs outlined above) doesn't support all SVG files as it doesn't support embedded raster images or text -- there might be other things as well but these are the things I know about. So I set about adding support for these as well. Supporting embedded raster images is easy, I just store the images on disk and then add code to the generated classes to read the images back when they are needed. Supporting all other unsupported elements (including text) is also easy -- I just convert the elements to images and then treat them like raster nodes. This isn't ideal as now they don't scale well and so by default raster images and other elements not supported by the original library are still not supported.

Anyway, enough waffle, I've wrapped up all the SVG to Java 2D code into a simple to use command line application (it can also be accessed via an API), the usage of which is given below

SVGBatchConverter: Convert SVG files into Java2D based classes

Usage: java -jar SVGRoundTrip.jar [OPTIONS] SVGFile1 ... SVGFileN
  -f       by defualt classes are not generated if they exist and are newer
           than the SVG files, use this option to force them to be regenerated
  -i       by default raster images embedded in SVG files are not supported
           enabling this option generates PNG files for raster nodes which
           are loaded and drawn as required by the generated code
  -n name  classname format -- by default the generated class will have the
           same name as the SVG file use this option to specify
           'prefix+suffix', if the format doesn't contain a + then it just
           specifies a prefix
  -o dir   output directory (required), this should be the root of the source
           tree the correct sub-directory will be used based upon specified
           package name
  -p name  the name of the package the generated classes should be a member of
  -t name  the template name or a file containing a template -- valid template
           names are currently 'plain' and 'flamingo', see the docs for details
           if unspecified the 'plain' template will be used
  -u name  specify how to treat unsupported SVG elements -- possible values are
           'fail' the default behaviour which causes conversion to fail
           'skip' unsupported elements will be skipped, conversion will succeed
           'image' unsupported elements will be converted to images to maintain
           the look of the original SVG file -- image support must (-i) must
           also be enabled for this to work

If directories are specified instead of single SVG files then all SVG files in
the directories will be converted
So now you know how I convert SVG files into Java 2D based classes for use in my applications. If you want to try this yourself then check out the code and have a go.

SVGRoundTrip, Part 2: In, Out, and Shake It All About!

I've done quite a bit more work on SVGRoundTrip, and while I don't yet want to highlight specific API features (as they may well change over the next few days), I did want to show an example.


This shows three version of a test application I've developed for SVGRoundTrip. The first image is a standard screen shot of the running test app, the second image is from the exported SVG file generated by the library (I loaded the SVG file into Inkscape and then exported a PNG), and the last image shows the exported SVG being loaded into a different test application and then saved as an image.

It's not perfect but, as work-in-progress, I'm pretty happy with the results.

SVGRoundTrip, Part 1: Why Use SVG?

Until recently, whenever I was designing the interface for a new application I'd usually use raster images, normally PNG files, for button icons etc. The advantage of raster images is that I can use many different graphics programs to edit the files and I have control over the appearance of every single pixel. The downside is that the images don't scale well. For example, lets say that I start out using 16x16 pixel images for toolbar buttons. I then realise that as I don't have many buttons it would make more sense to have bigger buttons, say 32x32 pixels. I could just scale the small images up but they would look horrid, so I'd have to go back and create a whole new set of images.

SVG files (Scalable Vector Graphics) do, however, scale well. Their ability to scale comes from the fact that instead of recording pixel information they store shape and colour information -- a square is stored by knowing the points of the four corners. This makes it easy to scale the images without losing quality. The downside is that there are less applications I can use to edit them (Inkscape is the best I've found) and more importantly Java has no native support for displaying them. Fortunately the Batik library from Apache can load and convert SVG files so I can use them in my applications. Unfortunately there are a couple of downsides to using Batik:
  1. The Batik distribution is huge, just the main JAR file weighs in at over 3MB. Given that I tend to release applications via Java Web Start I don't really want to add 3MB to the download time.
  2. Converting SVG files into a format that Java can display seems to be quite CPU and memory intensive, which leads to slow startup times as soon as you have just a few files to convert.
The Flamingo component suite (that I've used to develop the GUI for both PhotoGrid and 3DAssembler) sidesteps these issues by converting the SVG files into Java classes at development time. This means you don't need Batik at run-time (reducing the download size of the application) and the time/resources to convert the files is done once and not at each application startup. Perfect! Unfortunately the developer of Flamingo has recently announced that he is suspending support and development.

As I don't want to rely on code that is no longer being supported or developed, especially as I know there are some issues with the SVG related code, I've decided to start work on a new library called SVGRoundTrip. The library will be based on Batik and incorporate ideas from Flamingo with the aim of supporting both the display of SVG files in Java applications as well as the production of SVG files from within Java applications. I'm starting by adding support for things I need but will be happy to added support for more features as time goes by.

I'm going to write a number of posts highlighting different aspects of the library but for now I just wanted to show an example of why using SVG files is a much better idea than relying on raster image formats. The image on the left shows five different renderings of the same SVG file: 16x16, 32x32, 48x48, 64x64 and 200x200. The SVG file stores the 48x48 pixel version but as you can see there is no loss in quality as we scale up or down in size.

Whilst SVGRoundTrip is by no means finished the example image was actually produced using the library to load an SVG file into a Java application, draw it a different sizes and then save the image to a PNG file. If you can't wait until the next exciting installment of this blog then you can follow development from the SVGRoundTrip page on my Hudson server, or grab the source from the subversion repository.

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.

Hiding Hidden Objects

I thought I'd start this new blog with something simple -- a CSS layout problem that has bugged me for a long time and that I finally solved for the design of this blog.

When I've released applications through my main blog I've always tried to make them easy for people to use, and my approach has usually been to use Java Web Start to download, install and run the programs. This works really well as the deployment script checks for the correct version of Java and builds the appropriate links into the web page for me. I have, however, noticed that there are a couple of problems with the deployment script.

Firstly the JavaScript file cannot be included in the head section of the blog template with the other scripts I use. The problem is that the script embeds an object into the page at the point it is included. Objects can't be included in the head section of a HTML page so many browsers assume that if they find an object then you have forgotten the closing head tag and close the head section for you. This is annoying but easy to work around -- just include the script at the beginning of the page body.

The second problem is that although the object that is embedded is marked as hidden it still takes up a single line of vertical space, and of course depending on where you embed the script that could be a lot of space (imagine a 60pt title for instance).

It turns out that setting the hidden attribute to true is the same as setting the visibility CSS style to hidden -- the element isn't displayed but space is reserved for it. The trick, therefore, is to use CSS to turn off display of the element altogether. Adding the following CSS rule to your style sheet will do the trick and hide the Java deployment plugin so that it doesn't take up any space.
#deployJavaPlugin {
   visibility: hidden;
   display: none;
}
It isn't really necessary to set the visibility property but I like keeping it in for completeness.

Why this isn't done by the deployment script I don't know but at least now I have control over the white space in my layout.