]> gerrit.simantics Code Review - simantics/platform.git/commitdiff
Generate tidier SVG from diagrams 20/1120/5
authorJussi Koskela <jussi.koskela@semantum.fi>
Wed, 18 Oct 2017 10:20:30 +0000 (13:20 +0300)
committerTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Wed, 18 Oct 2017 11:52:08 +0000 (14:52 +0300)
Also moved rendering hints to more appropriate class.

refs #7557

Change-Id: If97b08ae82db111858b8350266b446b56114a30b

bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGGraphics2DWithPassthruSupport.java [new file with mode: 0644]
bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGShapeWithPassthruSupport.java [new file with mode: 0644]
bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DPDFRenderingHints.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DRenderingHints.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/SVGPassthruShape.java [new file with mode: 0644]

diff --git a/bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGGraphics2DWithPassthruSupport.java b/bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGGraphics2DWithPassthruSupport.java
new file mode 100644 (file)
index 0000000..a9f3f2a
--- /dev/null
@@ -0,0 +1,48 @@
+package org.simantics.diagram.svg.export;
+
+import java.awt.Font;
+import java.awt.RenderingHints;
+import java.awt.RenderingHints.Key;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.batik.svggen.SVGGeneratorContext;
+import org.apache.batik.svggen.SVGGeneratorContext.GraphicContextDefaults;
+import org.apache.batik.svggen.SVGGraphics2D;
+import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
+import org.simantics.scenegraph.g2d.G2DRenderingHints;
+import org.w3c.dom.Document;
+
+public class SVGGraphics2DWithPassthruSupport extends SVGGraphics2D {
+
+       public SVGGraphics2DWithPassthruSupport(Document document) {
+               this(SVGGeneratorContext.createDefault(document));
+       }
+       
+       public SVGGraphics2DWithPassthruSupport(SVGGeneratorContext context) {
+               super(updateDefaults(context), false);
+               
+               setRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE, G2DRenderingHints.TextRenderingMode.AS_TEXT);
+               setRenderingHint(G2DRenderingHints.KEY_SVG_PASSTHRU, true);
+               
+               this.shapeConverter = new SVGShapeWithPassthruSupport(generatorCtx);
+       }
+       
+       private static SVGGeneratorContext updateDefaults(SVGGeneratorContext context) {
+               GraphicContextDefaults gcDefaults = new GraphicContextDefaults();
+               
+        Map<Key, Object> hintMap = new HashMap<Key, Object>();
+               
+               hintMap.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+        hintMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+               hintMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+               hintMap.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
+               hintMap.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+               
+               gcDefaults.setRenderingHints(new RenderingHints(hintMap));
+               gcDefaults.setFont(Font.decode(null));
+               
+               context.setGraphicContextDefaults(gcDefaults);
+               return context;
+       }
+}
\ No newline at end of file
diff --git a/bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGShapeWithPassthruSupport.java b/bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGShapeWithPassthruSupport.java
new file mode 100644 (file)
index 0000000..584bcf5
--- /dev/null
@@ -0,0 +1,101 @@
+package org.simantics.diagram.svg.export;
+
+import java.awt.Shape;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.batik.svggen.SVGGeneratorContext;
+import org.apache.batik.svggen.SVGShape;
+import org.simantics.db.common.utils.Logger;
+import org.simantics.scenegraph.utils.SVGPassthruShape;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+public class SVGShapeWithPassthruSupport extends SVGShape {
+       private DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+       
+       public SVGShapeWithPassthruSupport(SVGGeneratorContext generatorCtx) {
+               super(generatorCtx);
+               dbf.setValidating(false);
+               dbf.setExpandEntityReferences(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) {
+                       Logger.defaultLogError(e);
+               }
+       }
+
+       public Element toSVG(Shape shape) {
+               if (shape instanceof SVGPassthruShape) {
+                       String source = ((SVGPassthruShape) shape).getSource();
+                       
+                       try {
+                               
+                               Document owner = generatorContext.getDOMFactory();
+
+                               @SuppressWarnings("unchecked")
+                               Map<String, String> defsMap = (Map<String, String>)owner.getUserData("defs-map");
+                               if (defsMap == null) {
+                                       defsMap = new HashMap<String, String>();
+                                       owner.setUserData("defs-map", defsMap, null);
+                               }
+                               
+                               synchronized (defsMap) {
+                                       String symbolId = defsMap.get(source);
+                                       if (symbolId == null) {
+                                               symbolId = "S" + defsMap.size();
+                                               defsMap.put(source, symbolId);
+                                               
+                                               DocumentBuilder db = dbf.newDocumentBuilder();
+                                               
+                                               Document doc = db.parse(new ByteArrayInputStream(source.getBytes("UTF-8")));
+                                               Node node = doc.getDocumentElement();
+                                               Node localNode = owner.importNode(node, true);
+
+                                               if (localNode instanceof Element) {
+                                                       Element g = generatorContext.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_G_TAG);
+                                                       Element e = (Element)localNode;
+                                                       e.setAttribute("id", symbolId);
+                                                       g.appendChild(localNode);
+                                                       return g;
+                                               } else {
+                                                       return null;
+                                               }
+                                       } else {
+                                               Element element = owner.createElement("use");
+                                               element.setAttribute("xlink:href", "#" + symbolId);
+                                               return element;
+                                       }
+                               }
+                       } catch (UnsupportedEncodingException e) {
+                               Logger.defaultLogError(e);
+                               return null;
+                       } catch (IOException e) {
+                               Logger.defaultLogError(e);
+                               return null;
+                       } catch (ParserConfigurationException e) {
+                               Logger.defaultLogError(e);
+                               return null;
+                       } catch (SAXException e) {
+                               Logger.defaultLogError(e);
+                               return null;
+                       }
+               } else {
+                       return super.toSVG(shape);
+               }
+               
+       }
+       
+}
index 9d39bbfdedccc4287b9eca1049d133c07c910da5..4fe3b51d898183c882b81b1feb14d7abd665ad44 100644 (file)
@@ -51,7 +51,8 @@ import org.simantics.scenegraph.LoaderNode;
 import org.simantics.scenegraph.ScenegraphUtils;
 import org.simantics.scenegraph.g2d.G2DNode;
 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
-import org.simantics.scenegraph.g2d.G2DPDFRenderingHints.TextRenderingMode;
+import org.simantics.scenegraph.g2d.G2DRenderingHints;
+import org.simantics.scenegraph.g2d.G2DRenderingHints.TextRenderingMode;
 import org.simantics.scenegraph.g2d.events.Event;
 import org.simantics.scenegraph.g2d.events.EventTypes;
 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
@@ -711,7 +712,7 @@ public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, L
 
         // PDF 
         PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
-        TextRenderingMode renderingMode = (TextRenderingMode) g.getRenderingHint(G2DPDFRenderingHints.KEY_TEXT_RENDERING_MODE);
+        TextRenderingMode renderingMode = (TextRenderingMode) g.getRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE);
         boolean renderAsText = writer != null || renderingMode == TextRenderingMode.AS_TEXT;
         /// PDF
 
index 0b9a57474057a41247b69c82e7fbf9e9846ab628..8cc16cfbcc126a3a841616e43eccd4a57c0b5174 100644 (file)
@@ -63,28 +63,4 @@ public final class G2DPDFRenderingHints {
         }
     };
 
-    /**
-     * If this hint is not specified, the default interpretation should be
-     * {@value #AS_PATHS}.
-     * 
-     * @since 1.31.0
-     */
-    public static enum TextRenderingMode {
-        AS_PATHS,
-        AS_TEXT
-    }
-
-    /**
-     * A rendering hint for telling text rendering Simantics G2D scene graph node
-     * implementations how to render the text: as text or paths.
-     * 
-     * @since 1.31.0
-     */
-    public static final Key KEY_TEXT_RENDERING_MODE = new Key(2004) {
-        @Override
-        public boolean isCompatibleValue(Object val) {
-            return val instanceof TextRenderingMode;
-        }
-    };
-
 }
\ No newline at end of file
index 0f1803e2d64ee6d27d74695cab015a62fa658b54..ff68dac5a692829ad157dc4180091dd9a3abb31a 100644 (file)
@@ -69,4 +69,40 @@ public final class G2DRenderingHints {
         }
     };
 
+    /**
+     * If this hint is not specified, the default interpretation should be
+     * {@value #AS_PATHS}.
+     * 
+     * @since 1.31.0
+     */
+    public static enum TextRenderingMode {
+        AS_PATHS,
+        AS_TEXT
+    }
+
+    /**
+     * A rendering hint for telling text rendering Simantics G2D scene graph node
+     * implementations how to render the text: as text or paths.
+     * 
+     * @since 1.31.0
+     */
+    public static final Key KEY_TEXT_RENDERING_MODE = new Key(2004) {
+        @Override
+        public boolean isCompatibleValue(Object val) {
+            return val instanceof TextRenderingMode;
+        }
+    };
+
+    /**
+     * Instead of rendering SVGNode using SVG Salamander pass it to G2D as SVGPassthruShape in String format.
+     * 
+     * @since 1.31.0
+     */
+    public static final Key KEY_SVG_PASSTHRU = new Key(2005) {
+        @Override
+        public boolean isCompatibleValue(Object val) {
+            return val instanceof Boolean;
+        }
+    };
+
 }
\ No newline at end of file
index 756804f7dc4e900c3d8ee2e66bb57ed3caf477f2..b2e973949517d7e66ff06391c7e1b20e27bd90fc 100644 (file)
@@ -28,11 +28,10 @@ import java.util.Arrays;
 
 import org.simantics.scenegraph.g2d.G2DNode;
 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
-import org.simantics.scenegraph.g2d.G2DPDFRenderingHints.TextRenderingMode;
+import org.simantics.scenegraph.g2d.G2DRenderingHints;
+import org.simantics.scenegraph.g2d.G2DRenderingHints.TextRenderingMode;
 import org.simantics.scenegraph.utils.GeometryUtils;
 
-import com.lowagie.text.pdf.PdfWriter;
-
 public class FlagNode extends G2DNode {
 
     private static final long          serialVersionUID = -1716729504104107151L;
@@ -314,7 +313,7 @@ public class FlagNode extends G2DNode {
             double y = startY;
             double textAreaWidth = textArea.getWidth() * gScaleRecip;
             boolean renderAsText = g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER) != null
-                    || g.getRenderingHint(G2DPDFRenderingHints.KEY_TEXT_RENDERING_MODE) == TextRenderingMode.AS_TEXT;
+                    || g.getRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE) == TextRenderingMode.AS_TEXT;
 
             for (int i = 0; i < flagText.length; ++i) {
                 //String line = flagText[i];
index d21ba1f16a73c91ee9517244492bfa6e2dccec2e..42ae2e4c93d34c9b8cdb48f502fb653a6975856f 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;
@@ -31,19 +33,33 @@ 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.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;
@@ -150,17 +166,25 @@ 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);
@@ -256,6 +280,65 @@ public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
         return dataHash;
     }
 
+    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);
+                    }
+                }
+            }
+
+            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;
diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/SVGPassthruShape.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/SVGPassthruShape.java
new file mode 100644 (file)
index 0000000..09fefd0
--- /dev/null
@@ -0,0 +1,113 @@
+package org.simantics.scenegraph.utils;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+public class SVGPassthruShape implements Shape {
+
+       private String source;
+       public final static Font DEFAULT_FONT = Font.decode(null);
+       
+       public SVGPassthruShape(String source) {
+               this.source = source;
+       }
+       
+       public String getSource() {
+               return source;
+       }
+       
+       public static void resetG2D(Graphics2D g2d) {
+               g2d.setColor(new Color(0, 0, 0));
+               g2d.setBackground(new Color(0, 0, 0));
+               g2d.setStroke(new BasicStroke());
+               g2d.setFont(DEFAULT_FONT);
+               QualityHints.HIGH_QUALITY_HINTS.setQuality(g2d);
+       }
+       
+       @Override
+       public Rectangle getBounds() {
+               return new Rectangle();
+       }
+
+       @Override
+       public Rectangle2D getBounds2D() {
+               return new Rectangle2D.Double();
+       }
+
+       @Override
+       public boolean contains(double x, double y) {
+               return false;
+       }
+
+       @Override
+       public boolean contains(Point2D p) {
+               return false;
+       }
+
+       @Override
+       public boolean intersects(double x, double y, double w, double h) {
+               return false;
+       }
+
+       @Override
+       public boolean intersects(Rectangle2D r) {
+               return false;
+       }
+
+       @Override
+       public boolean contains(double x, double y, double w, double h) {
+               return false;
+       }
+
+       @Override
+       public boolean contains(Rectangle2D r) {
+               return false;
+       }
+
+       @Override
+       public PathIterator getPathIterator(AffineTransform at) {
+               return new EmptyPathIterator();
+       }
+       
+       @Override
+       public PathIterator getPathIterator(AffineTransform at, double flatness) {
+               return new EmptyPathIterator();
+       }
+
+       private class EmptyPathIterator implements PathIterator {
+               
+               @Override
+               public void next() {
+                       
+               }
+               
+               @Override
+               public boolean isDone() {
+                       return true;
+               }
+               
+               @Override
+               public int getWindingRule() {
+                       return 0;
+               }
+               
+               @Override
+               public int currentSegment(double[] coords) {
+                       return 0;
+               }
+               
+               @Override
+               public int currentSegment(float[] coords) {
+                       return 0;
+               }
+       };
+
+}