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.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;
@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;
+ 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();
SVGDiagram d = diagramCache;
if (d != null) {
diagramCache = null;
+ dataHash = null;
SVGUniverse univ = SVGCache.getSVGUniverse();
if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
// Cleared!
}
this.data = cached;
this.defaultData = cached;
+ cleanDiagramCache();
}
@SyncField("targetSize")
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;
diagramCache = null;
}
- // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
- byte[] dataBytes = data.getBytes("UTF-8");
- dataHash = digest(dataBytes, assignments);
+ // 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());
-
- if (diagramCache.getRoot() == null) {
+ 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 = diagramCache.getRoot().getBoundingBox();
+ bbox = root.getBoundingBox();
if (bbox.isEmpty()) {
- univ.decRefCount(diagramCache.getXMLBase());
- diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
- dataHash = "empty";
- univ.incRefCount(diagramCache.getXMLBase());
- bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
+ // 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) diagramCache.getRoot().getBoundingBox().clone();
+ bbox = (Rectangle2D) root.getBoundingBox().clone();
} else {
bbox = (Rectangle2D) bbox.clone();
}
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) {
SVGElement e = diagram.getElement(ass.elementId);
if (e != null) {
if ("$text".equals(ass.attributeNameOrId)) {
if (e instanceof Tspan) {
Tspan t = (Tspan) e;
- t.setText(ass.value);
+ 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, 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;
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.
}
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;
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.
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();
}
}
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) {
} 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);
}
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;
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;
+ }
+ }
+
}