Using the Apache Batik toolkit for client- and server-side SVG processing

September 4, 2007

Cameron McCormack <cam@mcc.id.au>

Introduction

  • What is Batik?
    Tools
    Squiggle - Rasterizer - Font converter
    Modules
    SVG DOM - SVG microsyntax parsers
    Scripting module - Swing SVG component
    SVG generator - Transcoder

Squiggle, an example SVG browser

Features of Squiggle

  • (Mostly) conforming dynamic SVG 1.1 viewer
  • DOM viewer
  • Integrated ECMAScript debugger
  • Raster image exporter (PNG, JPEG, TIFF)
  • Supports a couple of assorted SVG 1.2 features
  • Upcoming enhancements to the DOM viewer and a new animation timeline viewer from Google Summer of Code students Ivan Andjelkovic and Jasleen Singh

JSVGCanvas

  • Interactive Swing SVG viewing component
  • Specify a URI or DOM tree to display
  • Built-in navigation interactors

Simple use of JSVGCanvas

// A canvas to show "squiggle.svg"
JSVGCanvas c = new JSVGCanvas();
String uri = new File("squiggle.svg").toURI().toString();
c.setURI(uri);

// A frame for the canvas to live in
JFrame f = new JFrame(uri);
f.setSize(600, 300);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Add the canvas to the frame
f.getContentPane().add(c);

// Show the frame
f.setVisible(true);

JSVGCanvas constructors

JSVGCanvas(SVGUserAgent ua,
           boolean eventsEnabled,
           boolean selectableText)

ua — Object that provides the canvas with access to the “outside world”

eventsEnabled — Whether UI interactions will dispatch DOM events

selectableText — Whether text in SVG documents can be highlighted with the mouse

JSVGCanvas()
Same as JSVGCanvas(null, true, true).

The SVGUserAgent interface

String getDefaultFontFamily()
The font to use when none is specified.
String getLanguages()
List of natural languages the user prefers.
String getPixelUnitToMillimeter()
Controls how length units translate to pixels.
String getUserStyleSheetURI()
User CSS style sheet.
void openLink(String uri, boolean newc)
Invoked when a link is clicked.
void showAlert(String message)
Invoked when script calls alert().

Supplying a user agent object

  • We want alert()ed messages to go to a JTextArea.
<text x='300' y='50' font-size='36' text-anchor='middle'>Click a circle:</text>
<g stroke='#444' stroke-width='5'>
  <circle cx='100' cy='130' r='40' fill='#f66' onclick='alert("You clicked red.")'/>
  <circle cx='300' cy='130' r='40' fill='#6f6' onclick='alert("You clicked green.")'/>
  <circle cx='500' cy='130' r='40' fill='#66f' onclick='alert("You clicked blue.")'/>
</g>

Supplying a user agent object

// A text area for messages
final JTextArea t = new JTextArea();
t.setPreferredSize(new Dimension(600, 100));
…

// A user agent object
SVGUserAgent ua = new SVGUserAgentAdapter() {
    public void showAlert(String message) {
        t.append(message + "\n");
    }
};

// A canvas to show "circles.svg"
JSVGCanvas c = new JSVGCanvas(ua, true, false);
String uri = new File("circles.svg").toURI().toString();
c.setURI(uri);

// A frame for the canvas to live in
JFrame f = new JFrame(uri);
f.getContentPane().setLayout(new BorderLayout());
f.getContentPane().add(c);
f.getContentPane().add(t, BorderLayout.SOUTH);
…

Interactors

  • React to AWT UI events on a JSVGCanvas
  • UI events aren’t passed on to the document
  • Implement org.apache.batik.swing.gvt.Interactor
    interface Interactor extends KeyListener, MouseListener,
                                 MouseMotionListener {
        
        boolean startInteraction(InputEvent ie);
    
        boolean endInteraction();
    }

Writing an interactor

  • We want to center the canvas on a double click

Writing an interactor

public class CenteringInteractor extends InteractorAdapter {

    protected boolean isDoubleClick(InputEvent ie) {
        if (ie instanceof MouseEvent) {
            MouseEvent me = (MouseEvent) ie;
            return me.getID() == MouseEvent.MOUSE_CLICKED
                    && me.getButton() == MouseEvent.BUTTON1
                    && me.getClickCount() == 2;
        }
        return false;
    }

    public boolean startInteraction(InputEvent ie) {
        return isDoubleClick(ie);
    }

    public boolean endInteraction() {
        return true;
    }

    ...

Writing an interactor

    public void mouseClicked(MouseEvent me) {
        if (isDoubleClick(me)) {
            JSVGCanvas c = (JSVGCanvas) me.getSource();
            int cx = c.getWidth() / 2;
            int cy = c.getHeight() / 2;
            int x = me.getX();
            int y = me.getY();
            AffineTransform at =
                AffineTransform.getTranslateInstance(cx - x, cy - y);
            AffineTransform rt =
                (AffineTransform) c.getRenderingTransform().clone();
            rt.preConcatenate(at);
            c.setRenderingTransform(rt);
        }
    }
}

JSVGCanvas c = ...;
c.getInteractors().add(new CenteringInteractor());

Overlays

  • Extra graphics painted above the canvas
  • Implement org.apache.batik.swing.gvt.Overlay
    interface Overlay {
        
        void paint(Graphics g);
    }

Writing an overlay

  • We want the map example to have a symbol marking the centre

Writing an overlay

public class CentreSymbolOverlay implements Overlay {

    protected JSVGCanvas canvas;

    public CentreSymbolOverlay(JSVGCanvas canvas) {
        this.canvas = canvas;
    }

    public void paint(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        int w = canvas.getWidth();
        int h = canvas.getHeight();
        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(2f));
        g2d.drawRect(w / 2 - 10, h / 2 - 10, 20, 20);
        g2d.drawLine(w / 2, h / 2 - 13, w / 2, h / 2 - 7);
        g2d.drawLine(w / 2, h / 2 + 7, w / 2, h / 2 + 13);
        g2d.drawLine(w / 2 - 13, h / 2, w / 2 - 7, h / 2);
        g2d.drawLine(w / 2 + 7, h / 2, w / 2 + 13, h / 2);
    }
}

JSVGCanvas c = ...;
c.getOverlays().add(new CentreSymbolOverlay(c));

Using the SVG DOM

  • An implementation of DOM Level 3 Core, DOM Level 3 Events (draft) and the SVG 1.1 DOM
  • Allows construction and manipulation of DOM trees representing SVG documents
  • Also has SVG specific functionality

Creating a document using the DOM

  • In a generic DOM implementation, to create a document you:
    • Get an instance of org.w3c.dom.DOMImplementation somehow
    • Call createDocument() on the DOMImplementation object to create a new object that implements org.w3c.dom.Document
    • Call various methods on the Document object and other newly created nodes to build the document
  • In Batik, the DOMImplementation object is obtained using
    org.apache.batik.dom.svg.SVGDOMImplementation
            .getDOMImplementation()

Creating a document using Batik

  • We want to construct this document from scratch:
    <svg xmlns='http://www.w3.org/2000/svg'
         width='400' height='450'>
    
      <rect x='10' y='20' width='100' height='50' fill='red'/>
    </svg>

Creating a document using Batik

// Get a DOMImplementation object
DOMImplementation impl = SVGDOMImplementation.getDOMImplementation();

// Create a new document
String svgNS = SVGDOMImplementation.SVG_NAMESPACE_URI;
Document doc = impl.createDocument(svgNS, "svg", null);

// Get the root element (the 'svg' element).
Element svgRoot = doc.getDocumentElement();

// Set the width and height attributes on the root 'svg' element.
svgRoot.setAttributeNS(null, "width", "400");
svgRoot.setAttributeNS(null, "height", "450");

// Create the rectangle.
Element rectangle = doc.createElementNS(svgNS, "rect");
rectangle.setAttributeNS(null, "x", "10");
rectangle.setAttributeNS(null, "y", "20");
rectangle.setAttributeNS(null, "width", "100");
rectangle.setAttributeNS(null, "height", "50");
rectangle.setAttributeNS(null, "fill", "red");

// Attach the rectangle to the root 'svg' element.
svgRoot.appendChild(rectangle);

Creating a document by parsing XML

  • We can parse XML from a file or from a string
  • The SAXSVGDocumentFactory class does the parsing
  • Supply it with a URI, InputStream or Reader
    createDocument(String uri)
    createDocument(String uri, InputStream inp)
    createDocument(String uri, Reader r)
  • The new Document objects are also org.w3c.dom.svg.SVGDocuments

Creating a document by parsing XML

String theXML = "<svg xmlns='http://www.w3.org/2000/svg' width='400' ...";

try {
    String parser = XMLResourceDescriptor.getXMLParserClassName();
    SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);

    // Construct a document from XML in a string
    StringReader r = new StringReader(theXML);
    Document doc1 = f.createDocument(r);

    // Construct a document from XML at a URI
    String uri = "http://xmlgraphics.apache.org/batik/demo/barChart.svg";
    Document doc2 = f.createDocument(uri);
} catch (IOException ioe) {
    // ...
}

Using the constructed document

We can:

  • Display and manipulate it in a JSVGCanvas
  • Use the SVG DOM to manipulate it without displaying it
  • Serialise it to XML
  • Rasterise it using the transcoder module (which we’ll see later)

Showing a Document in a JSVGCanvas

  • Use JSVGCanvas.setSVGDocument() to show an SVGDocument in a canvas
  • This is asynchronous
  • The canvas performs a number of steps, listeners can be attached for notification

Canvas rendering process

Rendering process steps:

  1. Building a DOM tree (but not if setSVGDocument() was used)
  2. Building a GVT tree
  3. Running initial scripts and dispatching the SVGLoad event
  4. Rendering the GVT tree
  5. Running the document (only if the document is dynamic)

A listener for each step: SVGDocumentLoaderListener, GVTTreeBuilderListener, SVGLoadEventDispatcherListener, GVTTreeRendererListener, UpdateManagerListener

Static vs dynamic documents

  • Static documents are rendered once to the canvas
  • Dynamic documents are watched for updates and re-rendered
  • JSVGCanvas defaults to auto-detecting based on content
  • Can force the mode with JSVGCanvas.setDocumentState(JSVGCanvas.ALWAYS_STATIC) or JSVGCanvas.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC)

Modifying documents in a JSVGCanvas

  • We can mutate the SVGDocument and cause the canvas to be updated
  • Must wait until the graphics tree has been built
  • Must set the canvas to be dynamic, if it would guess otherwise
  • Must perform updates in the update manager thread

Modifying documents in a JSVGCanvas

  • We want to rotate some text every 0.5 seconds

Modifying documents in a JSVGCanvas

The document to modify:

<svg xmlns='http://www.w3.org/2000/svg'
     width='400' height='300'
     font-size='32' font-family='"Bitstream Vera Sans"' text-anchor='middle'
     text-rendering='geometricPrecision' shape-rendering='geometricPrecision'>

  <g transform='translate(200,150)'>
    <g id='rotationGroup'>
      <rect x='-120' y='-24' width='240' height='48' rx='8'
            stroke='#478' stroke-width='3' fill='#cef'/>
      <text y='12'>Rotating text</text>
    </g>
  </g>
</svg>

Modifying documents in a JSVGCanvas

Listen for a SVGLoadEventDispatcherEvent to begin the rotations:

// A canvas to show "text.svg"
JSVGCanvas c = new JSVGCanvas();
c.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC);

// Timer task that will do the rotation
final RotationTimerTask tt = new RotationTimerTask(c);

// Start the timer when the document is ready
c.addSVGLoadEventDispatcherListener(new SVGLoadEventDispatcherAdapter() {
    public void svgLoadEventDispatchCompleted(SVGLoadEventDispatcherEvent e) {
        new Timer().scheduleAtFixedRate(tt, 500, 500);
    }
});

// Load the document
c.setURI(new File("text.svg").toURI().toString());

Modifying documents in a JSVGCanvas

class RotationTimerTask extends TimerTask {
    JSVGCanvas canvas;
    int angle;

    public RotationTimerTask(JSVGCanvas canvas) { this.canvas = canvas; }

    public void run() {
        // Increment the angle
        angle += 10;

        // Find the <g> element to rotate
        Document doc = canvas.getSVGDocument();
        final Element g = doc.getElementById("rotationGroup");

        // Schedule the rotation in the update manager's thread
        Runnable rotationUpdater = new Runnable() {
            public void run() {
                g.setAttributeNS(null, "transform", "rotate(" + angle + ")");
            }
        };
        canvas.getUpdateManager().getUpdateRunnableQueue().invokeLater
            (rotationUpdater);
    }
}

Using the SVG DOM with no rendering

  • Can build the document without a JSVGCanvas
  • Then you can use SVG DOM methods to perform SVG-specific actions
  • Process (“booting” the SVG DOM):
    1. Load/create the SVGDocument
    2. Create a BridgeContext
    3. Build the GVT tree

Using the SVG DOM with no rendering

In general:

import org.apache.batik.bridge.UserAgent;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.gvt.GraphicsNode;

SVGDocument    doc       = ...;
UserAgent      userAgent = new UserAgentAdapter();
DocumentLoader loader    = new DocumentLoader(userAgent);
BridgeContext  ctx       = new BridgeContext(userAgent, loader);
ctx.setDynamicState(BridgeContext.DYNAMIC);

GVTBuilder     builder   = new GVTBuilder();
GraphicsNode   rootGN    = builder.build(ctx, doc);

Using the SVG DOM with no rendering

We want to get the bounding box of an arbitrary text string:

<svg xmlns='http://www.w3.org/2000/svg'>
  <text x='100' y='100'>The text here</text>
</svg>

Using the SVG DOM with no rendering

String SVGNS            = "http://www.w3.org/2000/svg";
DOMImplementation impl  = SVGDOMImplementation.getDOMImplementation();
Document          doc   = impl.createDocument(SVGNS, "svg", null);
Element           text  = doc.createElementNS(SVGNS, "text");
text.setAttributeNS(null, "x", "100");
text.setAttributeNS(null, "y", "100");
text.appendChild(doc.createTextNode("The text here"));
doc.getDocumentElement().appendChild(text);

UserAgent      ua       = new UserAgentAdapter();
DocumentLoader loader   = new DocumentLoader(ua);
BridgeContext  ctx      = new BridgeContext(ua, loader);
ctx.setDynamicState(BridgeContext.DYNAMIC);

GVTBuilder     builder  = new GVTBuilder();
GraphicsNode   rootGN   = builder.build(ctx, doc);

SVGRect bbox = ((SVGLocatable) text).getBBox();
System.out.println
    ("(" + bbox.getX() + "," + bbox.getY() + ","
         + bbox.getWidth() + "," + bbox.getHeight() + ")");

Serialising a document

The org.apache.batik.svggen.XmlWriter class can serialise a Node

public static void writeXml(Node node,
                            Writer writer,
                            boolean escaped)

node — The DOM node to serialise

writer — Where to write the serialisation to

escaped — Whether non-ASCII characters should be written as numeric character references

Script interpreters

  • ECMAScript support is standard, provided by Rhino
  • Other languages can be supported

Using Java classes from ECMAScript

  • Rhino gives access to Java classes from ECMAScript
  • Use importClass() or importPackage() to bring the class into scope
  • Use new to create instances of the Java classes
  • Call methods on the instances

Using Java classes from ECMAScript

<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <circle cx="50" cy="50" r="50" fill="green" onclick="showFrame()"/>
  <script>
    importPackage(Packages.javax.swing);

    function showFrame() {
      var frame = new JFrame("My test frame");
      var label = new JLabel("Hello from Java objects created in ECMAScript!");
      label.setHorizontalAlignment(SwingConstants.CENTER);
      frame.getContentPane().add(label);
      frame.setSize(400, 100);
      frame.setVisible(true);
      frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    }
  </script>
</svg>

Binding objects

  • We can bind a Java object to a script variable from Java
  • Need access to the Interpreter object
  • JSVGCanvas has a BridgeContext, which has the interpreters for a document
  • Need to extend JSVGCanvas to get access to it

Accessing a bound object

  • We want to be able to maximize and un-maximize the frame the canvas lives in

Accessing a bound object

<svg xmlns='http://www.w3.org/2000/svg'
     width='400' height='300' viewBox='0 0 400 300'>

  <script><![CDATA[
    importClass(Packages.java.awt.Frame); 

    function maximizeFrame() {
      var newState;
      if (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) {
        newState = Frame.NORMAL;
      } else {
        newState = Frame.MAXIMIZED_BOTH;
      }
      frame.setExtendedState(newState);
    }
  ]]></script>

  <text x='200' y='150' fill='blue' text-decoration='underline'
        text-anchor='middle' font-size='32px' font-weight='bold'
        cursor='pointer' onclick='maximizeFrame()'>
    Click me!
  </text>
</svg>

Getting access to the Interpreter

The Interpreter interface has:

  void bindObject(String name, Object object);

Extend JSVGCanvas to get access to the Interpreter:

  class MyCanvas extends JSVGCanvas {

      public void bindObjectInES(String name, Object value) {
          Interpreter i = bridgeContext.getInterpreter("text/ecmascript");
          i.bindObject(name, value);
      }
  }

Binding an object

// A frame for the canvas to live in
final JFrame f = new JFrame("Binding example");
f.setSize(400, 300);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// A canvas to show "max.svg"
final MyCanvas c = new MyCanvas();

// When the document is loaded, bind the JFrame to the variable "frame"
c.addSVGLoadEventDispatcherListener(new SVGLoadEventDispatcherAdapter() {
    public void svgLoadEventDispatchCompleted(SVGLoadEventDispatcherEvent e) {
        c.bindObjectInES("frame", f);
    }
});
c.setURI(new File("max.svg").toURI().toString());

// Add the canvas to the frame
f.getContentPane().add(c);

// Show the frame
f.setVisible(true);

Evaluating script from Java

The Interpreter interface also has:

  Object evaluate(Reader scriptreader);
  Object evaluate(String script);

Again, we must extend JSVGCanvas to get at it.

  class MyCanvas extends JSVGCanvas {

      public Object evaluateInES(String script) {
          Interpreter i = bridgeContext.getInterpreter("text/ecmascript");
          return i.evaluate(script);
      }
  }

The SVGGraphics2D SVG generator

  • Implementation of Graphics2D that generates SVG
  • Easily export graphics to SVG
  • Allows customisation of SVG output

The SVGGraphics2D SVG generator

  • Using SVGGraphics2D is a three-step process:
    1. Create a Document to be used as a factory
    2. Paint on to the SVGGraphics2D
    3. Stream out the SVG content

SVGGraphics2D constructors

SVGGraphics2D(Document domFactory)
Default SVG creation behaviour.
SVGGraphics2D(Document domFactory, ImageHandler imageHandler,
              ExtensionHandler extensionHandler,
              boolean textAsShapes)
Custom behaviour when images are painted, and when custom paints/composites are used. Can set whether text is painted as shapes.
SVGGraphics2D(SVGGeneratorContext generatorCtx,
              boolean textAsShapes)
Custom non-graphics behaviour specified by the SVGGeneratorContext.

Simple example of SVGGraphics2D

// Get a DOMImplementation.
DOMImplementation domImpl =
    SVGDOMImplementation.getDOMImplementation();

// Create an instance of org.w3c.dom.Document.
String svgNS = "http://www.w3.org/2000/svg";
Document document = domImpl.createDocument(svgNS, "svg", null);

// Create an instance of the SVG Generator.
SVGGraphics2D svgGenerator = new SVGGraphics2D(document);

// Paint on the SVG Graphics2D implementation.
svgGenerator.setPaint(Color.red);
svgGenerator.fill(new Rectangle(10, 10, 100, 100));

// Finally, stream out SVG to the standard output using UTF-8 encoding.
boolean useCSS = true; // we want to use CSS style attributes
Writer out = new OutputStreamWriter(System.out, "UTF-8");
svgGenerator.stream(out, useCSS);

Simple customisation

  • You can set the comment text included in the SVG output
    DOMImplementation impl =
        GenericDOMImplementation.getDOMImplementation();
    String svgNS = "http://www.w3.org/2000/svg";
    Document myFactory = impl.createDocument(svgNS, "svg", null);
    
    SVGGeneratorContext ctx = SVGGeneratorContext.createDefault(myFactory);
    ctx.setComment("Generated by FooApplication with Batik SVG Generator");
    SVGGraphics2D g2d = new SVGGraphics2D(ctx, false);

Embedding fonts

  • You can instruct SVGGraphics2D to embed fonts
  • SVG <font> elements will be created
  • The font is subsetted

Embedding fonts

// Get a DOMImplementation.
DOMImplementation domImpl = SVGDOMImplementation.getDOMImplementation();

// Create an instance of org.w3c.dom.Document.
String svgNS = "http://www.w3.org/2000/svg";
Document document = domImpl.createDocument(svgNS, "svg", null);

// Create an instance of the SVG Generator with a context that has font embedding turned on.
SVGGeneratorContext ctx = SVGGeneratorContext.createDefault(document);
ctx.setEmbeddedFontsOn(true);
SVGGraphics2D svgGenerator = new SVGGraphics2D(ctx, false);

// Paint on the SVG Graphics2D implementation.
Font f = new Font("Trebuchet MS", Font.PLAIN, 36);
svgGenerator.setPaint(Color.blue);
svgGenerator.setFont(f);
svgGenerator.drawString("Some text!", 50, 50);

// Finally, stream out SVG to the standard output using UTF-8 encoding.
svgGenerator.stream(new OutputStreamWriter(System.out, "UTF-8"), false);

ttf2svg

  • Font conversion using the same classes that SVGGraphics2D uses
java -jar batik-ttf2svg.jar <ttf-path>
              [-l <range-begin>] [-h <range-end>]
              [-autorange]
              [-id <id>]
              [-o <output-path>]
              [-testcard]
          

Storing images differently

  • By default, SVGGraphics2D uses data: URIs
  • Can specify a different ImageHandler
  • Some prewritten ones:
    • ImageHandlerBase64Encoder (default)
    • ImageHandlerJPEGEncoder
    • ImageHandlerPNGEncoder
    • CachedImageHandlerBase64Encoder
    • CachedImageHandlerJPEGEncoder
    • CachedImageHandlerPNGEncoder

Using an ImageHandler

// Create a factory document.
DOMImplementation impl =
    GenericDOMImplementation.getDOMImplementation();
String svgNS = "http://www.w3.org/2000/svg";
Document myFactory = impl.createDocument(svgNS, "svg", null);

// Create the SVGGraphics2D with a custom ImageHandler.
SVGGeneratorContext ctx = SVGGeneratorContext.createDefault(myFactory);
ImageHandlerPNGEncoder ihandler =
    new ImageHandlerPNGEncoder("res/images", null);
ctx.setImageHandler(ihandler);
SVGGraphics2D g2d = new SVGGraphics2D(ctx, false);

// Paint on g2d.
...

Using a caching ImageHandler

// Create a factory document.
DOMImplementation impl =
    GenericDOMImplementation.getDOMImplementation();
String svgNS = "http://www.w3.org/2000/svg";
Document myFactory = impl.createDocument(svgNS, "svg", null);

// Create the SVGGraphics2D with a custom ImageHandler.
SVGGeneratorContext ctx = SVGGeneratorContext.createDefault(myFactory);
// Reuse our embedded base64-encoded image data.
CachedImageHandlerBase64Encoder ihandler =
    new CachedImageHandlerBase64Encoder();
ctx.setGenericImageHandler(ihandler);
SVGGraphics2D g2d = new SVGGraphics2D(ctx, false);

// Paint on g2d.
...

Transcoding

  • Support for converting SVG to PNG, JPEG, TIFF or PDF
  • Important interfaces/classes:
    • Transcoder
    • TranscoderInput
    • TranscoderOutput
    • TranscodingHints
    • ErrorHandler

A simple JPEG transcoder

// Create a JPEG transcoder.
JPEGTranscoder t = new JPEGTranscoder();

// Set the transcoding hints.
t.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(.8));

// Create the transcoder input.
String svgURI = new File(args[0]).toURL().toString();
TranscoderInput input = new TranscoderInput(svgURI);

// Create the transcoder output.
OutputStream ostream = new FileOutputStream("out.jpg");
TranscoderOutput output = new TranscoderOutput(ostream);

// Save the image.
t.transcode(input, output);

// Flush and close the stream.
ostream.flush();
ostream.close();

Transcoder hints

Use Transcoder.addTranscodingHint(TranscodingHints.Key, Object).

KeyValue type
ImageTranscoder.KEY_BACKGROUND_COLORPaint
SVGAbstractTranscoder.KEY_WIDTHFloat
SVGAbstractTranscoder.KEY_HEIGHTFloat
SVGAbstractTranscoder.KEY_MAX_HEIGHTFloat
SVGAbstractTranscoder.KEY_MAX_WIDTHFloat
SVGAbstractTranscoder.KEY_AOIRectangle2D
SVGAbstractTranscoder.KEY_MEDIAString
SVGAbstractTranscoder.KEY_ALTERNATE_STYLESHEETString
SVGAbstractTranscoder.KEY_USER_STYLESHEET_URIString
SVGAbstractTranscoder.KEY_PIXEL_TO_MMFloat
SVGAbstractTranscoder.KEY_LANGUAGEString
SVGAbstractTranscoder.KEY_EXECUTE_ONLOADBoolean
SVGAbstractTranscoder.KEY_SNAPSHOT_TIMEBoolean

Transcoding to PDF

  • The PDFTranscoder generates PDF documents
  • Written by the FOP team, but bundled with Batik
  • PDF-specific transcoding hints:
    KeyValue type
    PDFTranscoder.KEY_STROKE_TEXTBoolean
    PDFTranscoder.KEY_DEVICE_RESOLUTIONFloat
  • Not all SVG features currently map to PDF easily
  • Filters will be rasterised, complex text will become shapes

batik-rasterizer

  • Transcodes using the transcoder classes
  • Supports PDF too, despite the name
java -jar batik-rasterizer.jar
              [-d <output-dir-or-file>]
              [-m <output-mime-type>]
              [-w <width>] [-h <height>]
              [-a <x>,<y>,<w>,<h>]
              ...
              <input-files>

Transcoding on the server

  • Could invoke java -jar batik-rasterizer.jar ... for every document
  • Overhead of starting the JVM is too great
  • A Java servlet would be a better option
  • Extend javax.servlet.http.HttpServlet
  • Override void doGet(HttpServletRequest req, HttpServletResponse resp)

Servlet layout

transcoding-servlet/
WEB-INF/
classes/
TranscodingServlet.java
lib/
batik-anim.jar
batik-awt-util.jar
batik-bridge.jar
...
web.xml
index.html

web.xml configuration file

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
                      http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
  version="2.4">

  <display-name>Transcoding servlet</display-name>
  <description>
    A servlet that transcodes an SVG document to PNG
  </description>
  <servlet>
    <servlet-name>TranscodingServlet</servlet-name>
    <servlet-class>TranscodingServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>TranscodingServlet</servlet-name>
    <url-pattern>/transcode</url-pattern>
  </servlet-mapping>
</web-app>

TranscodingServlet.java

public class TranscodingServlet extends HttpServlet {
    
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
        throws IOException, ServletException {

        response.setContentType("image/png");

        String uri = request.getParameter("uri");
        int width = Integer.parseInt(request.getParameter("width"));
        int height = Integer.parseInt(request.getParameter("height"));

        PNGTranscoder t = new PNGTranscoder();
        t.addTranscodingHint(ImageTranscoder.KEY_WIDTH, new Float(width));
        t.addTranscodingHint(ImageTranscoder.KEY_HEIGHT, new Float(height));
        TranscoderInput input = new TranscoderInput(uri);
        TranscoderOutput output = new TranscoderOutput(response.getOutputStream());
        try {
            t.transcode(input, output);
        } catch (TranscoderException ex) {
            throw new ServletException(ex);
        }
    }
}

Batik in an applet

  • Batik can be used in an applet to provide SVG viewing capabilities
  • A “poor-man’s SVG plugin”
  • Drawbacks:
    • A hefty download
    • Same-origin restrictions
    • HTML communication is not perfect

HTML → applet communication

  • Public methods on the applet class are reflected on the <applet> DOM object
— HTML —
<html>
  ...
  <applet id="theApplet" code="...">
  </applet>
  <script>
    var theApplet = document.getElementById("theApplet");
    theApplet.f(123);
  </script>
</html>
— Java —
public class MyApplet extends JApplet {
  ...
  public void f(int x) {
    // ...
  }
}

Batik applet demo – HTML

http://xmlgraphics.apache.org/batik/demo.html

<applet archive="batik-anim.jar,batik-awt-util.jar,batik-bridge.jar,
                 batik-css.jar,batik-dom.jar,batik-ext.jar,
                 batik-gvt.jar,batik-parser.jar,batik-svg-dom.jar,
                 batik-script.jar,batik-swing.jar,batik-util.jar,
                 batik-xml.jar,xml-apis.jar,xml-apis-ext.jar"
        code="AppletDemo.class" codebase="demo/" mayscript="mayscript"
        width="400" height="300" id="chart">
</applet>
...
<input type="text" value="10" id="ShoeBar">
<input type="text" value="10" id="CarBar">
...
<button onclick="update()">Update chart</button>

Batik applet demo – script


function update() {
  updateBar("ShoeBar");
  updateBar("CarBar");
  updateBar("TravelBar");
  updateBar("ComputerBar");
}

function updateBar(name) {
  var chart = document.getElementById("chart");
  var input = document.getElementById(name);
  var value = Number(input.value);
  if (!isNaN(value) && value >= 0) {
    chart.updateBar(name, value);
  }
}

Batik applet demo – Java


public class AppletDemo extends JApplet {
  protected JSVGCanvas canvas;
  protected Document doc;
  protected Element svg;
  ...
  public void updateBar(final String name, final float value) {
    canvas.getUpdateManager().getUpdateRunnableQueue().invokeLater
      (new Runnable() {
        public void run() {
          Element bar = doc.getElementById(name);
          if (bar == null) {
            return;
          }

          Node n;
          Element path1, path2, path3;
          ... get the three <paths> that make up the bar ...

Batik applet demo – Java

          int offset;
          if (name.equals("ShoeBar")) {
            offset = 0;
          } else if (name.equals("CarBar")) {
            offset = 79;
          } else if (name.equals("TravelBar")) {
            offset = 158;
          } else {
            offset = 237;
          }

          String d = "M " + (offset + 86) + ",240 v -" + (3.7 * value) + ...;
          path1.setAttributeNS(null, "d", d);
          d = "M " + (offset + 86) + "," + (240 - 3.7 * value) + ...;
          path2.setAttributeNS(null, "d", d);
          d = "M " + (offset + 47) + "," + (240 - 3.7 * value) + " v " + ...;
          path3.setAttributeNS(null, "d", d);
        }
      });
  }
}

Applet → HTML communication

JSObject methods

public Object call(String methodName, Object args[])
Calls a method on an object.
public Object eval(String s)
Evaluates a fragment of script.
public Object getMember(String name)
Retrieves an object property.
public void setMember(String name, Object value)
Modifies an object property.

Modifying HTML from the applet

We want to use the circles example again, but to update the HTML instead of write to a JTextArea.

<div id="msg" style="font-size: 32px">Applet HTML example.</div>
<applet archive="lib/batik-anim.jar,lib/batik-awt-util.jar,lib/batik-bridge.jar,
                 lib/batik-css.jar,lib/batik-dom.jar,lib/batik-ext.jar,
                 lib/batik-gvt.jar,lib/batik-parser.jar,lib/batik-svg-dom.jar,
                 lib/batik-script.jar,lib/batik-swing.jar,lib/batik-util.jar,
                 lib/batik-xml.jar,lib/xml-apis.jar,lib/xml-apis-ext.jar"
        code="AppletHTMLExample.class" mayscript="mayscript"
        width="600" height="200">
</applet>

The circles2.svg document

<svg xmlns='http://www.w3.org/2000/svg'
     xmlns:xlink='http://www.w3.org/1999/xlink'
     width='600' height='200' viewBox='0 0 600 200'
     shape-rendering='geometricPrecision'
     text-rendering='geometricPrecision'>

  <text x='300' y='50' font-size='36' text-anchor='middle'>
    Click a circle:
  </text>
  <g stroke='#444' stroke-width='5'>
    <circle id='red' cx='100' cy='130' r='40' fill='#f66'/>
    <circle id='green' cx='300' cy='130' r='40' fill='#6f6'/>
    <circle id='blue' cx='500' cy='130' r='40' fill='#66f'/>
  </g>
</svg>

The AppletHTMLExample applet

protected JSVGCanvas canvas;
protected Document doc;

public void init() {
  // Create a canvas and load circles2.svg.
  canvas = new JSVGCanvas();
  getContentPane().add(canvas);
  try {
    String parser = XMLResourceDescriptor.getXMLParserClassName();
    SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
    URL url = new URL(getCodeBase(), "circles2.svg");
    doc = f.createDocument(url.toString());
    // Add event listeners to the circles.
    ((EventTarget) doc.getElementById("red"))
      .addEventListener("click", this, false);
    ((EventTarget) doc.getElementById("green"))
      .addEventListener("click", this, false);
    ((EventTarget) doc.getElementById("blue"))
      .addEventListener("click", this, false);
  } catch (Exception ex) {
  }
}

The AppletHTMLExample applet

public void start() {
  // Display the SVG document.
  canvas.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC);
  canvas.setDocument(doc);
}

public void stop() {
  // Remove the SVG document.
  canvas.setDocument(null);
}

public void handleEvent(Event evt) {
  // Update the HTML document when a circle is clicked.
  String name = ((Element) evt.getTarget()).getAttributeNS(null, "id");
  JSObject win = JSObject.getWindow(this);
  JSObject document = (JSObject) win.getMember("document");
  JSObject div =
    (JSObject) document.call("getElementById", new Object[] { "msg" });
  div.setMember("innerHTML", "You clicked the " + name + " circle.");
}

Thanks for listening!