From: Jussi Koskela Date: Wed, 18 Oct 2017 10:20:30 +0000 (+0300) Subject: Generate tidier SVG from diagrams X-Git-Tag: v1.31.0~109^2 X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=commitdiff_plain;h=e75a2a68817ab088db4f84419c2e988ec83fbd39 Generate tidier SVG from diagrams Also moved rendering hints to more appropriate class. refs #7557 Change-Id: If97b08ae82db111858b8350266b446b56114a30b --- 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 index 000000000..a9f3f2a04 --- /dev/null +++ b/bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGGraphics2DWithPassthruSupport.java @@ -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 hintMap = new HashMap(); + + 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 index 000000000..584bcf541 --- /dev/null +++ b/bundles/org.simantics.diagram.svg/src/org/simantics/diagram/svg/export/SVGShapeWithPassthruSupport.java @@ -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 defsMap = (Map)owner.getUserData("defs-map"); + if (defsMap == null) { + defsMap = new HashMap(); + 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); + } + + } + +} diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java index 9d39bbfde..4fe3b51d8 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java @@ -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 diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DPDFRenderingHints.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DPDFRenderingHints.java index 0b9a57474..8cc16cfbc 100644 --- a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DPDFRenderingHints.java +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DPDFRenderingHints.java @@ -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 diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DRenderingHints.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DRenderingHints.java index 0f1803e2d..ff68dac5a 100644 --- a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DRenderingHints.java +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/G2DRenderingHints.java @@ -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 diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java index 756804f7d..b2e973949 100644 --- a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/FlagNode.java @@ -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]; diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java index d21ba1f16..42ae2e4c9 100644 --- a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java @@ -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 assignments) { + try { + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(new InputSource(new StringReader(svg))); + + NodeList entries = doc.getElementsByTagName("*"); + for (int i=0; i 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 index 000000000..09fefd06f --- /dev/null +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/SVGPassthruShape.java @@ -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; + } + }; + +}