 |
SVG and Java UIs part 6: transcoding SVG to pure Java2D code
Posted by kirillcool on October 20, 2006 at 11:01 PM | Comments (31)
This entry is the sixth part in the ongoing series about using SVG-based icons in Swing-based applications.
- The first part outlined the need for using scalable icons in next-generation Java UIs and proposed using SVG images as a possible format, showing the ribbon component which employs heavy icon rescaling.
- The second entry introduced an SVG file previewer based on the breadcrumb bar component, a ribbon button from the ribbon component and SVG Salamander renderer
- The third part showed how the Apache Batik SVG renderer can initialize and scale SVG-based application in an asynchronous and non-blocking fashion.
- The fourth entry showed what happens beneath the hood and described some of the problems found with Batik integration.
- The fifth entry described the feedback from Batik developer and improvements made to the ribbon and SVG file previewer.
Over the past few months i have seen quite a few questions about converting SVG files to pure Java2D painting code. Since no such converter (to the best of my knowledge) exists (at least in the open-source world), this has been implemented in the latest drop of Flamingo project (release candidate of version 1.1 code-named Briana is scheduled for October 30). How do you run it? Very simple - click on the WebStart link below, grant all permissions (it needs read access to read the SVG files and write access to create the Java2D-based classes), use the breadcrumb bar to navigate to a folder that contains SVG files, wait for them to appear (they'll be loaded asynchronously) and just start clicking on the icons.
Clicking on an icon will create a Java class under the same folder with the same name (spaces and hyphens are replaced by the underscores). The class will have a single static paint method that gets a Graphics2D object and paints the icon.
Note that before you call this method, you can set any AffineTransform on the Graphics2D that you pass to the method in order to scale, shear or rotate the painting.

Here are few known limitations of this tool:
- The generated code requires Mustang to run, since it uses the
LinearGradientPaint and RadialGradientPaint mentioned in Chris's blog.
- Since Mustang's versions of these classes are a little different from Batik's implementation, some of the SVGs fail to transcode (when the stop fractions are not strictly increasing).
- The
TextNode is not supported. The support for this would involve writing and debugging tons of text-related code. I don't know about general usage, but Tango iconset uses this only on 2 out of 203 icons. If you want to chip in and provide the support - you're welcome.
Here is how you use the generated code:
public class Test extends JFrame {
public static class TestPanel extends JPanel {
@Override
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.translate(10, 10);
address_book_new.paint(g2);
g2.translate(50, 0);
g2.transform(AffineTransform.getScaleInstance(2.0, 2.0));
internet_web_browser.paint(g2);
g2.dispose();
}
}
public Test() {
super("SVG samples");
this.setLayout(new BorderLayout());
this.add(new TestPanel(), BorderLayout.CENTER);
this.setSize(180, 140);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Test().setVisible(true);
}
});
}
}
The address_book_new and internet_web_browser are the transcoded classes. Note that before the second icon is painted, i apply the scaling transformation (to illustrate how it is done). The result is:
Note how the second icon is scaled (relative to the first one).
The size of the generated code is comparable to the size of the original SVG file. The first icon in the above example takes 20KB in SVG format and 22KB in Java2D code. The second icon is 50KB in SVG format and 55KB in Java2D code. The generated code itself contains a few comments to help in mapping the Java2D sections to the corresponding SVG sections, but in general you don't need to look at it at all (as you wouldn't look at the SVG contents).
The implementation is quite straightforward. It uses the Apache Batik library (that's why the WebStart is so big) to load the SVG file and create a renderer tree (called GVT renderer tree). The nodes in this tree can be mapped directly to Java2D code. The only tricky part is in chaining and restoring transformations on nested nodes. In addition, some Batik classes do not provide getters for the relevant properties - i had to use reflection to obtain the field values.
As already mentioned, this tool has been successfully tested on the Tango iconset. Apart from two known issues (TextNode support and non-strictly increasing fractions), all the icons have been converted and displayed properly. If you're trying it on other SVG files and see UnsupportedOperationException, feel free to send me the relevant SVG file. Happy converting.
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
Excellent! That's a really cool tool!
Posted by: gfx on October 21, 2006 at 04:38 AM
-
FANTASTIC!
I applied it to the Kde Crystal Office Theme. It worked with a good number of the icons.
http://www.kde-look.org/content/show.php?content=30987
Posted by: aberrant on October 21, 2006 at 07:32 AM
-
aberrant,
Thanks for pointing me to this set. I just added support for Ellipse2D shape used by three of the icons in that set.
Posted by: kirillcool on October 21, 2006 at 09:05 AM
-
Cool stuff.
Maybe the size comparison would be more representative if it were between the compiled .class and the .svg or compressed svg (.svg.gz) ?
Posted by: liquid on October 22, 2006 at 04:24 AM
-
Romain, thanks
Liquid - the comparison would be then by taking compiled .class and the compressed .svg in the jar + the size of the external library that renders that SVG file. This would be dependent on the number of SVG files in the specific distribution. I'll leave this exercise to the readers.
Posted by: kirillcool on October 22, 2006 at 11:26 AM
-
Have you though about generating in-memory bytecode with BCEL that is now bundled with the JDK? This way, the application could "compile" the SVGs at startup time and avoid relying on classes whose names are mapped to the name of the SVG files. Hope that makes sense :)
Posted by: gfx on October 22, 2006 at 03:00 PM
-
Romain, this is a very interesting (and certainly doable) idea, but it would require having Batik in the application classpath - since i use it to parse the input file. And if you already have Batik, you can just use it for drawing the images.
Posted by: kirillcool on October 22, 2006 at 03:20 PM
-
Well, my problem with Batik is I remember it was slow. I was thinking that your generated code might be faster than using Batik for painting, but I might be wrong.
Posted by: gfx on October 22, 2006 at 03:51 PM
-
Romain,
Batik is indeed sometimes slow at first (loading the first image), but then it's pretty fast afterwards. Let me understand what you're proposing:
Have Batik load the SVG the first time it's used
Use BCEL / ASM / ... to generate a binary code (class) that paints that image
Return pointer to such a class to the calling code whenever the application needs to paint SVG
If that is correct, not only you'd have to have Batik + BCEL / ASM in your classpath, you'd also pay double "penalty" the first time (to generate the bytecode). The painting itself is not faster than Batik's, since it uses the same visitor to paint all the nodes - the only overhead removed is the method calls (everything is done in one big method). In addition, as far as i can tell, Batik's JSVG* Swing components cache the images as long as you don't change the current transformation.
Posted by: kirillcool on October 22, 2006 at 06:44 PM
-
VERY cool!
I've been looking for something almost like that, can it run from command line? (as part of the build process).
A couple of weeks ago I blogged about a tool to convert JSR 226 code into pure MIDP code, something like this can be used to create that. The main problem being that MIDP 2.x has nothing like Java 2D :-(
However, thats a huge head start towards something like that.
Posted by: vprise on October 23, 2006 at 07:42 AM
-
Shai,
It can run from command line, but you'll have to write a little bit of code :( The class is org.jvnet.flamingo.svg.SvgTranscoder. The constructor gets the URI of the original SVG file and the classname of the resulting Java class. Then, you call setListener(TranscoderListener listener) - this listener will be called when the transcoding (done asynchronously) is finished. After that, just call transcode to initiate the process.
Here is how it's done in the SvgFileViewPanel used in the WebStart link:
final PrintWriter pw = new PrintWriter(
javaClassFilename);
SvgTranscoder transcoder = new SvgTranscoder(
svgFile.toURI().toURL().toString(),
svgClassName);
transcoder.setListener(new TranscoderListener() {
public Writer getWriter() {
return pw;
}
public void finished() {
System.out.println("Finished with '"
+ javaClassFilename + "'");
}
});
transcoder.transcode();
Posted by: kirillcool on October 23, 2006 at 09:06 AM
-
Great, how hard is it to change the generated syntax so it will fit in a MIDP environment?
Posted by: vprise on October 23, 2006 at 10:36 AM
-
Shai - i have no idea since i don't work in that environment. The Java2D code is right here - you're welcome to read it and see how it can be adapted to your requirements.
Posted by: kirillcool on October 23, 2006 at 10:43 AM
-
Thanks, didn't phrase my question well ;-) I meant how easy it is to change the generated code, seems pretty easy.
Posted by: vprise on October 23, 2006 at 11:14 AM
-
In the sample of the generated code I see this:
public void paint(Graphics g) {
Is this intentional? Shouldn't it be paintComponent()?
Thanks,
Dmitri
Posted by: trembovetski on October 23, 2006 at 02:16 PM
-
Dmitri - this is most certainly intentional. The generated code doesn't contain any component, so why would it be called paintComponent? It's much closer to the paintIcon method of Icon interface in that it is component-agnostic.
Posted by: kirillcool on October 23, 2006 at 02:20 PM
-
Hmm. I guess I'm confused - the sample TestPanel class is derived from JPanel, which is then added to JFrame, no?
By overriding paint() instead of paintComponent(), wouldn't the code interfere with Swing's double-buffering (since the test is rendering directly to the screen instead of the Swing's backbuffer)?
Dmitri
Posted by: trembovetski on October 23, 2006 at 03:59 PM
-
Dmitri - i thought you were talking about the generated code (the code that actually paints the image). Since the sample panel doesn't contain anything, and it was for the demo only, i just overrode the paint() instead of paintComponent(). For this particular example it doesn't really matter - the panel is just a "canvas" for those two images. The real applications would most probably use the generated code inside the paintComponent() implementation - you are absolutely right about this.
Posted by: kirillcool on October 23, 2006 at 04:13 PM
-
Ok, got it. Just be careful with the code you post: someone may actually use it =)
Dmitri
Posted by: trembovetski on October 23, 2006 at 10:35 PM
-
Indeed - i fixed the sample code. Thanks.
Posted by: kirillcool on October 23, 2006 at 11:24 PM
-
I've tested the relative performance of classloading vs an XML/FI representation. Unexpectedly, classloading won! http://mernst.org/blog.
Posted by: mernst on October 24, 2006 at 02:25 PM
-
Matthias - am i missing something? I've looked at the source code, and saw no mention of SVG-related content at all. I thought you were going to compare generated Java2D drawing using loaded classes vs. Batik that loads the SVG XMLs on the fly.
Posted by: kirillcool on October 24, 2006 at 02:36 PM
-
Matthias - in response to the criticism of using generated code all i can say is that Batik is too large for just bundling a couple of vector-based icons. Using the generated Java2D code is pretty much the same as bundling the SVG files, since they convey the same exact data (and it's in human-readable format). The Java2D code is even easier to debug since you don't have to understand SVG format / learn Batik codebase.
Posted by: kirillcool on October 24, 2006 at 02:39 PM
-
Kirill, I understand that Batik is too slow and big and has some computation going on anyway. I'm not arguing for that. I was just saying that I would prefer some "data format" (render tree, rectangles, paths, ...) instead of compiled bytecode - especially because I thought it would be faster with classloading a particulary expensive part of the equation. But actually, it is not. When *drawing*, the compiled code runs fastest anyway.
I will try and run my tests with a real translated SVG.
Posted by: mernst on October 24, 2006 at 03:11 PM
-
It's good that Matthias is verifying what he declared. I don't like when people throw false conjectures in the air. I hope he will do the tests before posting, in the future. Because, although i was astonished by the first post, ... I tend to believe what he writes !
Great test code anyway.
Posted by: fenring on October 24, 2006 at 11:39 PM
-
Why not let the generated class implement javax.swing.Icon interface? Then it would be much easier to be used on swing componts.
Posted by: gtlang on October 25, 2006 at 12:05 AM
-
I'm spending way too much time on this but let me give you my latest results. I've now tested a more realistic dataset: the Java class produced by your transcoder on the preferences-desktop-accessibility icon vs an XML document containing the equivalent set of information (can send when interested). With this data, the table looks as follows:
test.ClassLoadingPerformance: 1016
test.FiBindingPerformance: 1048
test.FiDomBuildingPerformance: 672
test.SerializationPerformance: 703
So with more complex bytecode and data types, classloading and binding are up to par; serialization and DOM building are about 30 percent faster. The JAXB version spends about 40% of its time parsing attribute strings into doubles, BTW.
Enough of this. I would say there is no clear winner performancewise, so it's a matter of preference. Great work in any case!
Posted by: mernst on October 25, 2006 at 06:00 AM
-
gtlang - i thought about this, but it doesn't seem like a right thing to do. Unlike a regular Icon-implementing class, the generated code cab be freely scaled / rotated / sheared - since it's meant to be resolution-independent. Having the code to implement the Icon interface would imply that the painting is not vector-based (especially getIconWidth / getIconHeight methods). Now, I do have a ResizableIcon interface in flamingo which would be a much better match, but i didn't want to force this class onto the users of the generated code.
Posted by: kirillcool on October 25, 2006 at 10:05 AM
-
Great work, This not only reduce the application size, but also improves the
performance. Thanks alot Kirill.
I have modified the above code to show any files without modifying the class
everytime.
After generating svgFlower.class file from svgFlower.svg file
>java RunTest svgFlower
------------------------------------------------
import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.lang.reflect.*;
public class RunTest extends JFrame {
public static class TestPanel extends JPanel {
@Override
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.translate(10, 10);
//address_book_new.paint(g2);
//orange_gold_button.paint(g2);
callPaint(g2);
/*g2.translate(50, 0);
g2.transform(AffineTransform.getScaleInstance(2.0, 2.0));
internet_web_browser.paint(g2);*/
g2.dispose();
}
public static void callPaint(Graphics2D g2) {
try{
amethod.invoke(retobj, new Object[]{g2});
} catch(Exception e){System.out.println(e);}
}
}
public static void initializeClassReflection(String classname) throws Exception
{
System.out.println("::"+classname);
Class c = Class.forName(classname);
Object retobj = c.newInstance();
Class[] types = new Class[] { Graphics2D.class };
amethod = c.getMethod("paint", types);
}
public RunTest() {
super("SVG samples");
this.setLayout(new BorderLayout());
this.add(new TestPanel(), BorderLayout.CENTER);
this.setSize(180, 140);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
}
static Method amethod;
static Object retobj;
public static void main(String[] args) throws Exception {
if(args != null){
if(args.length <=0){
System.out.println("Please provide SVG-Java2D Class Name");
System.exit(0);
}else{
initializeClassReflection(args[0]);
}
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new RunTest().setVisible(true);
}
});
}
}
------------------------------------------------
Kishore.
Posted by: kishoresjava on July 14, 2007 at 07:18 AM
-
Hi, i know this is quit old but i am very interested on this project
First : the WebStart is not working so i read the jnlp and downloaded all jars and java was complaining about SwingWorker and i downloaded it and added it to cp
The application itself is kinda cool although it crashes when reading oxygen/scalable/actions looks like it doesn't like to read a folder with thousands of svgz files
i converted some icons and i may say I love this app :p
Now about the geneated code...
i think it should be more optimized lets say have a function to generate the paths and keep it on a var and stop keeping generating paths over and over again
Maybe create a private method for each shape
Other things that i miss
getWidth |
getHeight |> this can be read from the svg i thing for example oxygen icons are in 128x128
paint(Graphics2D g) just paint as it do now
paint(Graphics2D g, int width, int height) paint it but scale it to the specified size
paint(Graphics2D g,int x,int y,int width,int height) this one might not be need but still would be nice
Icon getIcon() returns an Icon with the graphics painted on it
Icon getIcon(int width, int height) same but let you resize
Imagine how cool would be to do new JLabel(MyIcon.getIcon(32,32));
I hope you think about it
Posted by: porfirioribeiro on November 27, 2007 at 08:03 AM
-
porfirioribeiro - many thanks for your comments and suggestions. Hopefully i will be able to address most of them in the next version of Flamingo. In the meantime, you can follow my new blog at Pushing Pixels that tracks the progress of my Java desktop projects (including Flamingo).
Posted by: kirillcool on November 29, 2007 at 05:00 PM
|