]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Reset SVGNode's data hash on clean up
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / SVGNode.java
index e88c7b94c5fb9dec196eff9216923a6fbf533dd3..f8f692e1d88b70ee080c6adb1591ba5a24d3a70c 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
- * in Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
 package org.simantics.scenegraph.g2d.nodes;
-\r
-import java.awt.Graphics2D;\r
-import java.awt.Point;\r
-import java.awt.geom.AffineTransform;\r
-import java.awt.geom.Rectangle2D;\r
-import java.io.ByteArrayInputStream;\r
-import java.io.IOException;\r
-import java.lang.ref.WeakReference;\r
-import java.math.BigInteger;\r
-import java.net.URI;\r
-import java.net.URL;\r
-import java.security.MessageDigest;\r
-import java.security.NoSuchAlgorithmException;\r
-import java.util.ArrayList;\r
-import java.util.HashMap;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.WeakHashMap;\r
-\r
-import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;\r
-import org.simantics.scenegraph.LoaderNode;\r
-import org.simantics.scenegraph.ScenegraphUtils;\r
-import org.simantics.scenegraph.g2d.G2DNode;\r
-import org.simantics.scenegraph.utils.BufferedImage;\r
-import org.simantics.scenegraph.utils.G2DUtils;\r
-import org.simantics.scenegraph.utils.InitValueSupport;\r
-import org.simantics.scenegraph.utils.MipMapBufferedImage;\r
-import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;\r
-import org.simantics.scenegraph.utils.VRamBufferedImage;\r
-import org.simantics.scl.runtime.function.Function1;\r
-import org.simantics.scl.runtime.function.Function2;\r
-import org.simantics.utils.threads.AWTThread;\r
-\r
-import com.kitfox.svg.SVGCache;\r
-import com.kitfox.svg.SVGDiagram;\r
-import com.kitfox.svg.SVGElement;\r
-import com.kitfox.svg.SVGException;\r
-import com.kitfox.svg.SVGRoot;\r
-import com.kitfox.svg.SVGUniverse;\r
-import com.kitfox.svg.Text;\r
-import com.kitfox.svg.Tspan;\r
-import com.kitfox.svg.animation.AnimationElement;\r
-\r
+
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
+import org.simantics.scenegraph.LoaderNode;
+import org.simantics.scenegraph.ScenegraphUtils;
+import org.simantics.scenegraph.g2d.G2DNode;
+import org.simantics.scenegraph.g2d.G2DRenderingHints;
+import org.simantics.scenegraph.utils.BufferedImage;
+import org.simantics.scenegraph.utils.G2DUtils;
+import org.simantics.scenegraph.utils.InitValueSupport;
+import org.simantics.scenegraph.utils.MipMapBufferedImage;
+import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
+import org.simantics.scenegraph.utils.SVGPassthruShape;
+import org.simantics.scenegraph.utils.VRamBufferedImage;
+import org.simantics.scl.runtime.function.Function1;
+import org.simantics.scl.runtime.function.Function2;
+import org.simantics.utils.threads.AWTThread;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import com.kitfox.svg.RenderableElement;
+import com.kitfox.svg.SVGCache;
+import com.kitfox.svg.SVGDiagram;
+import com.kitfox.svg.SVGElement;
+import com.kitfox.svg.SVGException;
+import com.kitfox.svg.SVGRoot;
+import com.kitfox.svg.SVGUniverse;
+import com.kitfox.svg.Text;
+import com.kitfox.svg.Tspan;
+import com.kitfox.svg.animation.AnimationElement;
+
 @RasterOutputWidget
 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
-\r
-       public static class SVGNodeAssignment {\r
-               public String elementId;\r
-               public String attributeNameOrId;\r
-               public String value;\r
-               public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) {\r
-                       this.elementId = elementId;\r
-                       this.attributeNameOrId = attributeNameOrId;\r
-                       this.value = value;\r
-               }\r
-               @Override\r
-               public int hashCode() {\r
-                       final int prime = 31;\r
-                       int result = 1;\r
-                       result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode());\r
-                       result = prime * result + ((elementId == null) ? 0 : elementId.hashCode());\r
-                       result = prime * result + ((value == null) ? 0 : value.hashCode());\r
-                       return result;\r
-               }\r
-               @Override\r
-               public boolean equals(Object obj) {\r
-                       if (this == obj)\r
-                               return true;\r
-                       if (obj == null)\r
-                               return false;\r
-                       if (getClass() != obj.getClass())\r
-                               return false;\r
-                       SVGNodeAssignment other = (SVGNodeAssignment) obj;\r
-                       if (attributeNameOrId == null) {\r
-                               if (other.attributeNameOrId != null)\r
-                                       return false;\r
-                       } else if (!attributeNameOrId.equals(other.attributeNameOrId))\r
-                               return false;\r
-                       if (elementId == null) {\r
-                               if (other.elementId != null)\r
-                                       return false;\r
-                       } else if (!elementId.equals(other.elementId))\r
-                               return false;\r
-                       if (value == null) {\r
-                               if (other.value != null)\r
-                                       return false;\r
-                       } else if (!value.equals(other.value))\r
-                               return false;\r
-                       return true;\r
-               }\r
-       }\r
-       
-    private static final long serialVersionUID = 8508750881358776559L;
-
-    protected String          data             = null;\r
-    protected String          defaultData      = null;\r
-    protected Point           targetSize       = null;\r
-    protected Boolean         useMipMap        = true;\r
-    protected Rectangle2D     bounds           = null;\r
-    \r
-    protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();\r
-\r
-    transient BufferedImage buffer    = null;\r
-    transient String documentCache    = null;\r
-    transient SVGDiagram diagramCache = null;\r
-    transient String dataHash         = null;\r
-\r
-    static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();\r
-\r
-    @Override\r
-    public void cleanup() {\r
-        cleanDiagramCache();\r
-    }\r
-\r
-    public void setAssignments(List<SVGNodeAssignment> ass) {\r
-       assignments.clear();\r
-       assignments.addAll(ass);\r
-    }\r
-    \r
-    public void cleanDiagramCache() {\r
-        SVGDiagram d = diagramCache;\r
-        if (d != null) {\r
-            diagramCache = null;\r
-            SVGUniverse univ = SVGCache.getSVGUniverse();\r
-            if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {\r
-                // Cleared!\r
-                //System.out.println("cleared: " + d.getXMLBase());\r
-            }\r
-        }\r
-    }\r
-\r
-    static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();\r
-\r
+
+       private static final long serialVersionUID = 8508750881358776559L;
+
+       private static final Logger LOGGER = LoggerFactory.getLogger(SVGNode.class);
+    protected String          data             = null;
+    protected String          defaultData      = null;
+    protected Point           targetSize       = null;
+    protected Boolean         useMipMap        = true;
+    protected Rectangle2D     bounds           = null;
+
+    protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
+
+    protected transient BufferedImage buffer    = null;
+    protected transient String documentCache    = null;
+    protected transient SVGDiagram diagramCache = null;
+    protected transient String dataHash         = null;
+
+    static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
+
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Override
+    public void cleanup() {
+        cleanDiagramCache();
+    }
+
+    public void setAssignments(List<SVGNodeAssignment> ass) {
+       assignments.clear();
+       assignments.addAll(ass);
+    }
+    
+    public void cleanDiagramCache() {
+        SVGDiagram d = diagramCache;
+        if (d != null) {
+            diagramCache = null;
+            dataHash = null;
+            SVGUniverse univ = SVGCache.getSVGUniverse();
+            if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
+                // Cleared!
+                //System.out.println("cleared: " + d.getXMLBase());
+            }
+        }
+    }
+
+    static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
+
     @PropertySetter("SVG")
     @SyncField("data")
-    public void setData(String data) {\r
-        String cached = dataCache.get(data);\r
-        if (cached == null) {\r
-            cached = data;\r
-            dataCache.put(data, data);\r
-        }\r
-        this.data = cached;\r
-        this.defaultData = cached;\r
+    public void setData(String data) {
+        String cached = dataCache.get(data);
+        if (cached == null) {
+            cached = data;
+            dataCache.put(data, data);
+        }
+        this.data = cached;
+        this.defaultData = cached;
+        cleanDiagramCache();
     }
 
     @SyncField("targetSize")
@@ -168,188 +151,335 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
     @SyncField("useMipMap")
     public void useMipMap(Boolean use) {
         this.useMipMap = use;
-    }\r
-\r
+    }
+
     @PropertySetter("Bounds")
     @SyncField("bounds")
     public void setBounds(Rectangle2D bounds) {
         this.bounds = bounds;
     }
 
-    @Override\r
+    @Override
     public Rectangle2D getBoundsInLocal() {
-        if (bounds == null)\r
-            parseSVG();\r
+        if (bounds == null)
+            parseSVG();
         return bounds;
     }
 
     @Override
     public void render(Graphics2D g2d) {
-        if (data == null)\r
-            return; // Not initialized\r
-\r
-        if (!data.equals(documentCache) || diagramCache == null || buffer == null)\r
-            initBuffer(g2d);
-\r
-        AffineTransform ot = null;\r
-        if (!transform.isIdentity()) {\r
-            ot = g2d.getTransform();\r
-            g2d.transform(transform);\r
+        if (data == null)
+            return; // Not initialized
+
+        AffineTransform ot = null;
+        if (!transform.isIdentity()) {
+            ot = g2d.getTransform();
+            g2d.transform(transform);
+        }
+
+        if (g2d.getRenderingHint(G2DRenderingHints.KEY_SVG_PASSTHRU) == Boolean.TRUE) {
+            SVGPassthruShape.resetG2D(g2d);
+            String svg = assignments.isEmpty() ? data : applyAssigments(data, assignments);
+            if (svg != null) {
+                g2d.fill(new SVGPassthruShape(svg));
+            }
+        } else {
+            if (!data.equals(documentCache) || diagramCache == null || buffer == null)
+                initBuffer(g2d);
+
+            if (buffer != null)
+                buffer.paint(g2d);
         }
 
-        if (buffer != null)\r
-            buffer.paint(g2d);\r
-\r
-        if (ot != null)\r
-            g2d.setTransform(ot);\r
+        if (ot != null)
+            g2d.setTransform(ot);
+    }
+
+    protected int dynamicHash() {
+        return 0;
     }
 
     protected String parseSVG() {
-        if (data == null)\r
-            return null;\r
-\r
-        try {\r
-               SVGUniverse univ = SVGCache.getSVGUniverse();\r
-               synchronized(univ) {
-                       // NOTE: hard-coded to assume all SVG data is encoded in UTF-8\r
-                       byte[] dataBytes = data.getBytes("UTF-8");
-                       dataHash = digest(dataBytes, assignments);\r
-                       if (diagramCache != null)\r
-                               univ.decRefCount(diagramCache.getXMLBase());\r
-                       URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
-                       diagramCache = univ.getDiagram(uri, false);\r
-                       if (diagramCache != null) {\r
-                               if (diagramCache.getRoot() == null) {\r
-                                       diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);\r
-                                       dataHash = "broken";\r
-                               } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {\r
-                                       diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);\r
-                                       dataHash = "empty";\r
-                               } else {\r
-                                       for(SVGNodeAssignment ass : assignments) {\r
-                                               SVGElement e = diagramCache.getElement(ass.elementId);\r
-                                               if(e != null) {\r
-                                                       if("$text".equals(ass.attributeNameOrId)) {\r
-                                                               Tspan t = (Tspan)e;\r
-                                                               t.setText(ass.value);\r
-                                                               Text text = (Text)t.getParent();\r
-                                                               text.rebuild();\r
-                                                       } else {\r
-                                                               e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);\r
-                                                       }\r
-                                               }\r
-                                       }\r
-                                       if(!assignments.isEmpty())\r
-                                               diagramCache.updateTime(0);\r
-                               }\r
-                       }
-                       documentCache = data;\r
-                       if (diagramCache != null) {\r
-                               setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone());\r
-                               univ.incRefCount(diagramCache.getXMLBase());\r
-                       } else {\r
-                               setBounds(new Rectangle2D.Double());\r
-                       }\r
-               }\r
-        } catch (SVGException e) {\r
-            setBounds((Rectangle2D) diagramCache.getViewRect().clone());\r
-        } catch (IOException e) {\r
+        if (data == null)
+            return null;
+
+        SVGUniverse univ = SVGCache.getSVGUniverse();
+        try {
+            Rectangle2D bbox = null;
+            synchronized (univ) {
+                // Relinquish reference to current element
+                if (diagramCache != null) {
+                    univ.decRefCount(diagramCache.getXMLBase());
+                    diagramCache = null;
+                }
+
+                // Lets check for rootAssignment that contributes the whole SVG
+                SVGNodeAssignment rootAssignment = null;
+                if (!assignments.isEmpty()) {
+                    for (SVGNodeAssignment ass : assignments) {
+                        if (ass.attributeNameOrId.equals("$root")) {
+                            rootAssignment = ass;
+                            break;
+                        }
+                    }
+                }
+                byte[] dataBytes;
+                if (rootAssignment != null) {
+                    dataBytes = rootAssignment.value.getBytes("UTF-8");
+                } else {
+                    // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
+                    dataBytes = data.getBytes("UTF-8");
+                }
+                dataHash = digest(dataBytes, assignments, dynamicHash());
+                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
+                diagramCache = univ.getDiagram(uri, false);
+
+                if (diagramCache != null) {
+                    univ.incRefCount(diagramCache.getXMLBase());
+                    SVGRoot root = diagramCache.getRoot();
+                    if (root == null) {
+                        univ.decRefCount(diagramCache.getXMLBase());
+                        diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
+                        dataHash = "broken";
+                        univ.incRefCount(diagramCache.getXMLBase());
+                        bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
+                    } else {
+                        bbox = root.getBoundingBox();
+                        if (bbox.isEmpty()) {
+                            // Lets check if this should be visible or not
+                            Set<?> presentationAttributes = root.getPresentationAttributes();
+                            if (!presentationAttributes.contains("display")) {
+                                // TODO: fix this - How can one read values of attributes in SVG salamander???
+                                univ.decRefCount(diagramCache.getXMLBase());
+                                diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
+                                dataHash = "empty";
+                                univ.incRefCount(diagramCache.getXMLBase());
+                                bbox = (Rectangle2D) root.getBoundingBox().clone();
+                            } else {
+                                bbox = new Rectangle2D.Double(0, 0, 0, 0);
+                            }
+                        } else {
+                            if (applyAssignments(diagramCache, assignments)) {
+                                bbox = (Rectangle2D) root.getBoundingBox().clone();
+                            } else {
+                                bbox = (Rectangle2D) bbox.clone();
+                            }
+                        }
+                    }
+                } else {
+                    bbox = new Rectangle2D.Double();
+                }
+            }
+
+            documentCache = data;
+            setBounds(bbox);
+        } catch (SVGException e) {
+            // This can only occur if diagramCache != null.
+            setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
+            univ.decRefCount(diagramCache.getXMLBase());
+            diagramCache = null;
+        } catch (IOException e) {
             diagramCache = null;
         }
 
         return dataHash;
     }
-\r
-    public static Rectangle2D getBounds(String data) {\r
-       return getBounds(data, null);\r
-    }\r
-\r
-    public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {\r
-        if (data == null) {\r
-            new Exception("null SVG data").printStackTrace();\r
-            return null;\r
-        }\r
-\r
-        SVGDiagram diagramCache = null;\r
-        try {\r
-            // NOTE: hard-coded to assume all SVG data is encoded in UTF-8\r
-            byte[] dataBytes = data.getBytes("UTF-8");\r
-            String digest = digest(dataBytes, assignments);\r
-\r
-            SVGUniverse univ = SVGCache.getSVGUniverse();\r
-            // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.\r
-            synchronized (univ) {\r
-                //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);\r
-                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);\r
-                diagramCache = univ.getDiagram(uri, false);\r
-                if (diagramCache != null) {\r
-                    if (diagramCache.getRoot() == null) {\r
-                        diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));\r
-                    } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {\r
-                        diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));\r
-                    }\r
-                }\r
-            }\r
-\r
-            Rectangle2D rect = null;\r
-            if (diagramCache != null) {\r
-                SVGRoot root = diagramCache.getRoot();\r
-                Rectangle2D bbox = root.getBoundingBox();\r
-                rect = (Rectangle2D) bbox.clone();\r
-            } else {\r
-                rect = new Rectangle2D.Double();\r
-            }\r
-            return rect;\r
-        } catch (SVGException e) {\r
-            return ((Rectangle2D) diagramCache.getViewRect().clone());\r
-        } catch(IOException e) {\r
-        }\r
-        return null;\r
-    }\r
-\r
-    public static Rectangle2D getRealBounds(String data) {\r
-       return getRealBounds(data, null);\r
-    }\r
-
-    public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {\r
-        if (data == null) {\r
-            new Exception("null SVG data").printStackTrace();\r
-            return null;\r
-        }\r
-\r
-        SVGDiagram diagramCache = null;\r
-        try {\r
-            // NOTE: hard-coded to assume all SVG data is encoded in UTF-8\r
-            byte[] dataBytes = data.getBytes("UTF-8");\r
-            String digest = digest(dataBytes, assignments);\r
-\r
-            SVGUniverse univ = SVGCache.getSVGUniverse();\r
-            // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.\r
-            synchronized (univ) {\r
-                //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);\r
-                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);\r
-                diagramCache = univ.getDiagram(uri, false);\r
-                if (diagramCache != null) {\r
-                       SVGRoot root = diagramCache.getRoot(); \r
-                    if (root == null) return new Rectangle2D.Double();\r
-                    return (Rectangle2D)root.getBoundingBox().clone();\r
-                }\r
-            }\r
-        } catch (SVGException e) {\r
-            return ((Rectangle2D) diagramCache.getViewRect().clone());\r
-        } catch(IOException e) {\r
-        }\r
-        return null;\r
-    }\r
-\r
-    protected void initBuffer(Graphics2D g2d) {\r
-       \r
-        if (!data.equals(documentCache) || diagramCache == null) {\r
-               dataHash = parseSVG();
-               if (diagramCache == null) {
-                       System.err.println("UNABLE TO PARSE SVG:\n" + data);
-                       return;
-               }\r
+
+    private static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+    static {
+        dbf.setValidating(false);
+        try {
+            dbf.setFeature("http://xml.org/sax/features/namespaces", false);
+            dbf.setFeature("http://xml.org/sax/features/validation", false);
+            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
+            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+        } catch (ParserConfigurationException e) {
+            // Nothing to do
+        }
+    }
+
+    // Notice: Remember to change both implementations of applyAssigments when modifying the functionality.
+    protected static String applyAssigments(String svg, List<SVGNodeAssignment> assignments) {
+        try {
+            DocumentBuilder db = dbf.newDocumentBuilder();
+            Document doc = db.parse(new InputSource(new StringReader(svg)));
+
+            NodeList entries = doc.getElementsByTagName("*");
+            for (int i=0; i<entries.getLength(); i++) {
+                Element element = (Element) entries.item(i);
+                if (element.hasAttribute("id")) {
+                    element.setIdAttribute("id", true);
+                }
+            }
+            for (SVGNodeAssignment ass : assignments) {
+                Element e = doc.getElementById(ass.elementId);
+                if (e != null) {
+                    if ("$text".equals(ass.attributeNameOrId)) {
+                        if (e.getTagName().equals("tspan")) {
+                            if (ass.value.trim().isEmpty()) {
+                                e.setTextContent("-");
+                            } else {
+                                e.setTextContent(ass.value);
+                            }
+                        }
+                    } else if (ass.attributeNameOrId.startsWith("#")) {
+                        e.setAttribute(ass.attributeNameOrId.substring(1), ass.value);
+                    } else {
+                        e.setAttribute(ass.attributeNameOrId, ass.value);
+                    }
+                } else {
+                    LOGGER.warn("Element with id='" + ass.elementId + " was not found.");
+                }
+            }
+
+            DOMSource domSource = new DOMSource(doc);
+            StringWriter writer = new StringWriter();
+            StreamResult result = new StreamResult(writer);
+            TransformerFactory tf = TransformerFactory.newInstance();
+            Transformer transformer = tf.newTransformer();
+            transformer.transform(domSource, result);
+            return writer.toString();
+
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    // Notice: Remember to change both implementations of applyAssigments when modifying the functionality.
+    protected boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
+        if (assignments.isEmpty())
+            return false;
+
+        boolean changed = false;
+
+        // Without this elements are sometimes not found by id!
+        diagram.updateTime(0);
+
+        for (SVGNodeAssignment ass : assignments) {
+            SVGElement e = diagram.getElement(ass.elementId);
+            if (e != null) {
+                if ("$text".equals(ass.attributeNameOrId)) {
+                    if (e instanceof Tspan) {
+                        Tspan t = (Tspan) e;
+                        if (ass.value.trim().isEmpty()) {
+                               t.setText("-");
+                        } else {
+                               t.setText(ass.value);
+                        }
+                        SVGElement parent = t.getParent();
+                        if (parent instanceof Text)
+                            ((Text) parent).rebuild();
+                        changed = true;
+                    }
+                } else if (ass.attributeNameOrId.startsWith("#")) {
+                    e.setAttribute(ass.attributeNameOrId.substring(1), AnimationElement.AT_CSS, ass.value);
+                    changed = true;
+                } else {
+                    e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
+                    changed = true;
+                }
+            }
+        }
+
+        // Without this the attribute values are not correctly reflected in rendering
+        diagram.updateTime(0);
+
+        return changed;
+    }
+
+    public static Rectangle2D getBounds(String data) {
+        return getBounds(data, 0);
+    }
+
+    public static Rectangle2D getBounds(String data, int dynamicHash) {
+        return getBounds(data, Collections.emptyList(), dynamicHash);
+    }
+
+    public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
+        if (data == null) {
+            new Exception("null SVG data").printStackTrace();
+            return null;
+        }
+
+        SVGDiagram diagramCache = null;
+        try {
+            // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
+            byte[] dataBytes = data.getBytes("UTF-8");
+            String digest = digest(dataBytes, assignments, dynamicHash);
+
+            SVGUniverse univ = SVGCache.getSVGUniverse();
+            // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
+            synchronized (univ) {
+                //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
+                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
+                diagramCache = univ.getDiagram(uri, false);
+                if (diagramCache != null) {
+                    if (diagramCache.getRoot() == null) {
+                        diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
+                    } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
+                        diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
+                    }
+                }
+            }
+
+            Rectangle2D rect = null;
+            if (diagramCache != null) {
+                SVGRoot root = diagramCache.getRoot();
+                Rectangle2D bbox = root.getBoundingBox();
+                rect = (Rectangle2D) bbox.clone();
+            } else {
+                rect = new Rectangle2D.Double();
+            }
+            return rect;
+        } catch (SVGException e) {
+            return diagramCache.getViewRect(new Rectangle2D.Double());
+        } catch(IOException e) {
+        }
+        return null;
+    }
+
+    public static Rectangle2D getRealBounds(String data) {
+        return getRealBounds(data, Collections.emptyList(), 0);
+    }
+
+    public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
+        if (data == null) {
+            new Exception("null SVG data").printStackTrace();
+            return null;
+        }
+
+        SVGDiagram diagramCache = null;
+        try {
+            // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
+            byte[] dataBytes = data.getBytes("UTF-8");
+            String digest = digest(dataBytes, assignments, dynamicHash);
+
+            SVGUniverse univ = SVGCache.getSVGUniverse();
+            // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
+            synchronized (univ) {
+                //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
+                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
+                diagramCache = univ.getDiagram(uri, false);
+                if (diagramCache != null) {
+                    SVGRoot root = diagramCache.getRoot();
+                    if (root == null) return new Rectangle2D.Double();
+                    return (Rectangle2D)root.getBoundingBox().clone();
+                }
+            }
+        } catch (SVGException e) {
+            return diagramCache.getViewRect(new Rectangle2D.Double());
+        } catch(IOException e) {
+        }
+        return null;
+    }
+
+    protected void initBuffer(Graphics2D g2d) {
+        if (!data.equals(documentCache) || diagramCache == null) {
+            dataHash = parseSVG();
+            if (diagramCache == null) {
+                LOGGER.warn("UNABLE TO PARSE SVG:\n" + data);
+                return;
+            }
         }
 
         if (buffer != null) {
@@ -360,15 +490,15 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
             buffer = bufferCache.get(dataHash).get();
         } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
             buffer = null;
-        } else if(useMipMap) {\r
-               if(G2DUtils.isAccelerated(g2d)) {
+        } else if(useMipMap) {
+            if(G2DUtils.isAccelerated(g2d)) {
                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
             } else {
                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
             }
             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
         } else {
-               if(G2DUtils.isAccelerated(g2d)) {
+            if(G2DUtils.isAccelerated(g2d)) {
                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
             } else {
                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
@@ -376,72 +506,88 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
         }
     }
-\r
-    public void setProperty(String field, Object value) {\r
-        if("data".equals(field)) {\r
-//             System.out.println("SVGNode data -> " + value);\r
-            this.data = (String)value;\r
-        } else if ("z".equals(field)) {\r
-//             System.out.println("SVGNode z -> " + value);\r
-            setZIndex((Integer)value);\r
-        } else if ("position".equals(field)) {\r
-//             System.out.println("SVGNode position -> " + value);\r
-            Point point = (Point)value;\r
-            setTransform(AffineTransform.getTranslateInstance(point.x, point.y));\r
-//             setPosition(point.x, point.y);\r
-        }\r
-    }\r
-\r
-    @Override\r
-    public void initValues() {\r
-        data = defaultData;\r
-        dataHash = null;\r
-        buffer =  null;\r
-    }\r
-\r
-    static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();\r
-    \r
-    static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {\r
-        try {\r
-            MessageDigest md = MessageDigest.getInstance("MD5");\r
-            byte[] messageDigest = md.digest(dataBytes);\r
-            BigInteger number = new BigInteger(1, messageDigest);\r
-            String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);\r
-            String result = digestCache.get(dataHash);\r
-            if(result == null) {\r
-               result = dataHash;\r
-               digestCache.put(dataHash,dataHash);\r
-            }\r
-            return result;\r
-        } catch (NoSuchAlgorithmException e) {\r
-            // Shouldn't happen\r
-            throw new Error("MD5 digest must exist.");\r
-        }\r
-    }\r
-\r
-    static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");\r
-    static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");\r
-\r
-       @Override\r
-       public Function1<Object, Boolean> getPropertyFunction(String propertyName) {\r
-               return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);\r
-       }\r
-       \r
-       @Override\r
-       public <T> T getProperty(String propertyName) {\r
-               return null;\r
-       }\r
-\r
-       @Override\r
-       public void setPropertyCallback(Function2<String, Object, Boolean> callback) {\r
-       }\r
-\r
-       public void synchronizeDocument(String document) {\r
-               setData(document);\r
-       }\r
-\r
-       public void synchronizeTransform(double[] data) {\r
-               this.setTransform(new AffineTransform(data));\r
-       }\r
+
+    public void setProperty(String field, Object value) {
+        if("data".equals(field)) {
+//             System.out.println("SVGNode data -> " + value);
+            this.data = (String)value;
+        } else if ("z".equals(field)) {
+//             System.out.println("SVGNode z -> " + value);
+            setZIndex((Integer)value);
+        } else if ("position".equals(field)) {
+//             System.out.println("SVGNode position -> " + value);
+            Point point = (Point)value;
+            setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
+//             setPosition(point.x, point.y);
+        }
+    }
+
+    @Override
+    public void initValues() {
+        data = defaultData;
+        dataHash = null;
+        buffer =  null;
+    }
+
+    static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
+
+    static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments, int dynamicHash) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] messageDigest = md.digest(dataBytes);
+            BigInteger number = new BigInteger(1, messageDigest);
+            String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0) + 31 * dynamicHash;
+            String result = digestCache.get(dataHash);
+            if(result == null) {
+               result = dataHash;
+               digestCache.put(dataHash,dataHash);
+            }
+            return result;
+        } catch (NoSuchAlgorithmException e) {
+            // Shouldn't happen
+            throw new Error("MD5 digest must exist.");
+        }
+    }
+
+    static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
+    static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
+
+       @Override
+       public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
+               return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
+       }
        
+       @Override
+       public <T> T getProperty(String propertyName) {
+               return null;
+       }
+
+       @Override
+       public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
+       }
+
+       public void synchronizeDocument(String document) {
+               setData(document);
+       }
+
+       public void synchronizeTransform(double[] data) {
+               this.setTransform(new AffineTransform(data));
+       }
+
+       public String getSVGText() {
+               String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
+               //return diagramCache.toString();
+               //return data.replace("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"><svg xmlns=\"http://www.w3.org/2000/svg\" overflow=\"visible\" version=\"1.1\"", "<g").replaceAll("svg>", "/g>");
+               return ret;
+       }
+
+    public Rectangle2D getElementBounds(String id) throws SVGException {
+        SVGElement e = diagramCache.getElement(id);
+        if (e instanceof RenderableElement) {
+            return ((RenderableElement)e).getBoundingBox();
+        } else {
+           return null;
+        }
+    }
+
 }