X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.scenegraph%2Fsrc%2Forg%2Fsimantics%2Fscenegraph%2Fg2d%2Fnodes%2FSVGNode.java;h=f8f692e1d88b70ee080c6adb1591ba5a24d3a70c;hp=e88c7b94c5fb9dec196eff9216923a6fbf533dd3;hb=81e3522f6cb5822f701b66a9fbf9be9f9bf97ec9;hpb=969bd23cab98a79ca9101af33334000879fb60c5 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 e88c7b94c..f8f692e1d 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 @@ -1,158 +1,141 @@ -/******************************************************************************* - * 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 - *******************************************************************************/ +/******************************************************************************* + * 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; - -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.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.HashMap; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; - -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.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.VRamBufferedImage; -import org.simantics.scl.runtime.function.Function1; -import org.simantics.scl.runtime.function.Function2; -import org.simantics.utils.threads.AWTThread; - -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; - + +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 { - - public static class SVGNodeAssignment { - public String elementId; - public String attributeNameOrId; - public String value; - public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) { - this.elementId = elementId; - this.attributeNameOrId = attributeNameOrId; - this.value = value; - } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode()); - result = prime * result + ((elementId == null) ? 0 : elementId.hashCode()); - result = prime * result + ((value == null) ? 0 : value.hashCode()); - return result; - } - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SVGNodeAssignment other = (SVGNodeAssignment) obj; - if (attributeNameOrId == null) { - if (other.attributeNameOrId != null) - return false; - } else if (!attributeNameOrId.equals(other.attributeNameOrId)) - return false; - if (elementId == null) { - if (other.elementId != null) - return false; - } else if (!elementId.equals(other.elementId)) - return false; - if (value == null) { - if (other.value != null) - return false; - } else if (!value.equals(other.value)) - return false; - return true; - } - } - - private static final long serialVersionUID = 8508750881358776559L; - - protected String data = null; - protected String defaultData = null; - protected Point targetSize = null; - protected Boolean useMipMap = true; - protected Rectangle2D bounds = null; - - protected List assignments = new ArrayList(); - - transient BufferedImage buffer = null; - transient String documentCache = null; - transient SVGDiagram diagramCache = null; - transient String dataHash = null; - - static transient Map> bufferCache = new HashMap>(); - - @Override - public void cleanup() { - cleanDiagramCache(); - } - - public void setAssignments(List ass) { - assignments.clear(); - assignments.addAll(ass); - } - - public void cleanDiagramCache() { - SVGDiagram d = diagramCache; - if (d != null) { - diagramCache = null; - SVGUniverse univ = SVGCache.getSVGUniverse(); - if (univ.decRefCountAndClear(d.getXMLBase()) == 0) { - // Cleared! - //System.out.println("cleared: " + d.getXMLBase()); - } - } - } - - static WeakHashMap dataCache = new WeakHashMap(); - + + 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 assignments = new ArrayList(); + + protected transient BufferedImage buffer = null; + protected transient String documentCache = null; + protected transient SVGDiagram diagramCache = null; + protected transient String dataHash = null; + + static transient Map> bufferCache = new HashMap>(); + + @Override + public void init() { + super.init(); + } + + @Override + public void cleanup() { + cleanDiagramCache(); + } + + public void setAssignments(List 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 dataCache = new WeakHashMap(); + @PropertySetter("SVG") @SyncField("data") - 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; + 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; - } - + } + @PropertySetter("Bounds") @SyncField("bounds") public void setBounds(Rectangle2D bounds) { this.bounds = bounds; } - @Override + @Override public Rectangle2D getBoundsInLocal() { - if (bounds == null) - parseSVG(); + if (bounds == null) + parseSVG(); return bounds; } @Override public void render(Graphics2D g2d) { - 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 (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) - buffer.paint(g2d); - - if (ot != null) - g2d.setTransform(ot); + if (ot != null) + g2d.setTransform(ot); + } + + protected int dynamicHash() { + return 0; } protected String parseSVG() { - if (data == null) - return null; - - try { - SVGUniverse univ = SVGCache.getSVGUniverse(); - synchronized(univ) { - // NOTE: hard-coded to assume all SVG data is encoded in UTF-8 - byte[] dataBytes = data.getBytes("UTF-8"); - dataHash = digest(dataBytes, assignments); - if (diagramCache != null) - univ.decRefCount(diagramCache.getXMLBase()); - URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash); - diagramCache = univ.getDiagram(uri, false); - if (diagramCache != null) { - if (diagramCache.getRoot() == null) { - diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false); - dataHash = "broken"; - } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) { - diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false); - dataHash = "empty"; - } else { - for(SVGNodeAssignment ass : assignments) { - SVGElement e = diagramCache.getElement(ass.elementId); - if(e != null) { - if("$text".equals(ass.attributeNameOrId)) { - Tspan t = (Tspan)e; - t.setText(ass.value); - Text text = (Text)t.getParent(); - text.rebuild(); - } else { - e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value); - } - } - } - if(!assignments.isEmpty()) - diagramCache.updateTime(0); - } - } - documentCache = data; - if (diagramCache != null) { - setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone()); - univ.incRefCount(diagramCache.getXMLBase()); - } else { - setBounds(new Rectangle2D.Double()); - } - } - } catch (SVGException e) { - setBounds((Rectangle2D) diagramCache.getViewRect().clone()); - } catch (IOException e) { + 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; } - - public static Rectangle2D getBounds(String data) { - return getBounds(data, null); - } - - public static Rectangle2D getBounds(String data, List assignments) { - 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); - - 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 ((Rectangle2D) diagramCache.getViewRect().clone()); - } catch(IOException e) { - } - return null; - } - - public static Rectangle2D getRealBounds(String data) { - return getRealBounds(data, null); - } - - public static Rectangle2D getRealBounds(String data, List assignments) { - 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); - - 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 ((Rectangle2D) diagramCache.getViewRect().clone()); - } catch(IOException e) { - } - return null; - } - - 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; - } + + 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 getPropertyFunction(String propertyName) { - return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName); - } - - @Override - public T getProperty(String propertyName) { - return null; - } - - @Override - public void setPropertyCallback(Function2 callback) { - } - - public void synchronizeDocument(String document) { - setData(document); - } - - public void synchronizeTransform(double[] data) { - this.setTransform(new AffineTransform(data)); - } + + 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 digestCache = new WeakHashMap(); + + static String digest(byte[] dataBytes, List 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 getPropertyFunction(String propertyName) { + return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName); + } + @Override + public T getProperty(String propertyName) { + return null; + } + + @Override + public void setPropertyCallback(Function2 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("", "g>"); + //return diagramCache.toString(); + //return data.replace("", "/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; + } + } + }