]> 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 ec85fd75c86e7893a8b7341bcfde6641e380d244..f8f692e1d88b70ee080c6adb1591ba5a24d3a70c 100644 (file)
@@ -17,6 +17,8 @@ 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;
@@ -24,26 +26,44 @@ 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;
@@ -59,21 +79,27 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
 
        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>();
 
-    transient BufferedImage buffer    = null;
-    transient String documentCache    = null;
-    transient SVGDiagram diagramCache = null;
-    transient String dataHash         = null;
+    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();
@@ -88,6 +114,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         SVGDiagram d = diagramCache;
         if (d != null) {
             diagramCache = null;
+            dataHash = null;
             SVGUniverse univ = SVGCache.getSVGUniverse();
             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
                 // Cleared!
@@ -108,6 +135,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         }
         this.data = cached;
         this.defaultData = cached;
+        cleanDiagramCache();
     }
 
     @SyncField("targetSize")
@@ -143,22 +171,34 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         if (data == null)
             return; // Not initialized
 
-        if (!data.equals(documentCache) || diagramCache == null || buffer == null)
-            initBuffer(g2d);
-
         AffineTransform ot = null;
         if (!transform.isIdentity()) {
             ot = g2d.getTransform();
             g2d.transform(transform);
         }
 
-        if (buffer != null)
-            buffer.paint(g2d);
+        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 (ot != null)
             g2d.setTransform(ot);
     }
 
+    protected int dynamicHash() {
+        return 0;
+    }
+
     protected String parseSVG() {
         if (data == null)
             return null;
@@ -173,8 +213,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
                     diagramCache = null;
                 }
 
-                
-                // Lets check for rootAssignment that contributes the whole SVG 
+                // Lets check for rootAssignment that contributes the whole SVG
                 SVGNodeAssignment rootAssignment = null;
                 if (!assignments.isEmpty()) {
                     for (SVGNodeAssignment ass : assignments) {
@@ -191,7 +230,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
                     // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
                     dataBytes = data.getBytes("UTF-8");
                 }
-                dataHash = digest(dataBytes, assignments);
+                dataHash = digest(dataBytes, assignments, dynamicHash());
                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
                 diagramCache = univ.getDiagram(uri, false);
 
@@ -208,7 +247,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
                         bbox = root.getBoundingBox();
                         if (bbox.isEmpty()) {
                             // Lets check if this should be visible or not
-                            Set presentationAttributes = root.getPresentationAttributes();
+                            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());
@@ -246,14 +285,77 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         return dataHash;
     }
 
-    private static boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
+    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) {
-//             System.err.println("assign: " + ass.elementId + " " + ass.attributeNameOrId + " " + ass.value);
-//             if("opacity".equals(ass.attributeNameOrId))
-//                     System.err.println("faaf");
             SVGElement e = diagram.getElement(ass.elementId);
             if (e != null) {
                 if ("$text".equals(ass.attributeNameOrId)) {
@@ -269,21 +371,31 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
                             ((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, null);
+        return getBounds(data, 0);
     }
 
-    public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
+    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;
@@ -293,7 +405,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         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);
+            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.
@@ -327,10 +439,10 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
     }
 
     public static Rectangle2D getRealBounds(String data) {
-       return getRealBounds(data, null);
+        return getRealBounds(data, Collections.emptyList(), 0);
     }
 
-    public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
+    public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
         if (data == null) {
             new Exception("null SVG data").printStackTrace();
             return null;
@@ -340,7 +452,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         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);
+            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.
@@ -349,7 +461,7 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
                 diagramCache = univ.getDiagram(uri, false);
                 if (diagramCache != null) {
-                    SVGRoot root = diagramCache.getRoot(); 
+                    SVGRoot root = diagramCache.getRoot();
                     if (root == null) return new Rectangle2D.Double();
                     return (Rectangle2D)root.getBoundingBox().clone();
                 }
@@ -362,13 +474,12 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
     }
 
     protected void initBuffer(Graphics2D g2d) {
-       
         if (!data.equals(documentCache) || diagramCache == null) {
-               dataHash = parseSVG();
-               if (diagramCache == null) {
-                       System.err.println("UNABLE TO PARSE SVG:\n" + data);
-                       return;
-               }
+            dataHash = parseSVG();
+            if (diagramCache == null) {
+                LOGGER.warn("UNABLE TO PARSE SVG:\n" + data);
+                return;
+            }
         }
 
         if (buffer != null) {
@@ -380,14 +491,14 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
             buffer = null;
         } else if(useMipMap) {
-               if(G2DUtils.isAccelerated(g2d)) {
+            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);
@@ -419,13 +530,13 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
     }
 
     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
-    
-    static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
+
+    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);
+            String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0) + 31 * dynamicHash;
             String result = digestCache.get(dataHash);
             if(result == null) {
                result = dataHash;
@@ -462,12 +573,21 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
        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;
+        }
+    }
+
 }