1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.scenegraph.g2d.nodes;
14 import java.awt.Graphics2D;
15 import java.awt.Point;
16 import java.awt.geom.AffineTransform;
17 import java.awt.geom.Rectangle2D;
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.io.StringReader;
21 import java.io.StringWriter;
22 import java.lang.ref.WeakReference;
23 import java.math.BigInteger;
26 import java.security.MessageDigest;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.List;
34 import java.util.WeakHashMap;
36 import javax.xml.parsers.DocumentBuilder;
37 import javax.xml.parsers.DocumentBuilderFactory;
38 import javax.xml.parsers.ParserConfigurationException;
39 import javax.xml.transform.Transformer;
40 import javax.xml.transform.TransformerFactory;
41 import javax.xml.transform.dom.DOMSource;
42 import javax.xml.transform.stream.StreamResult;
44 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
45 import org.simantics.scenegraph.LoaderNode;
46 import org.simantics.scenegraph.ScenegraphUtils;
47 import org.simantics.scenegraph.g2d.G2DNode;
48 import org.simantics.scenegraph.g2d.G2DRenderingHints;
49 import org.simantics.scenegraph.utils.BufferedImage;
50 import org.simantics.scenegraph.utils.G2DUtils;
51 import org.simantics.scenegraph.utils.InitValueSupport;
52 import org.simantics.scenegraph.utils.MipMapBufferedImage;
53 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
54 import org.simantics.scenegraph.utils.SVGPassthruShape;
55 import org.simantics.scenegraph.utils.VRamBufferedImage;
56 import org.simantics.scl.runtime.function.Function1;
57 import org.simantics.scl.runtime.function.Function2;
58 import org.simantics.utils.threads.AWTThread;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.w3c.dom.Document;
62 import org.w3c.dom.Element;
63 import org.w3c.dom.NodeList;
64 import org.xml.sax.InputSource;
66 import com.kitfox.svg.RenderableElement;
67 import com.kitfox.svg.SVGCache;
68 import com.kitfox.svg.SVGDiagram;
69 import com.kitfox.svg.SVGElement;
70 import com.kitfox.svg.SVGException;
71 import com.kitfox.svg.SVGRoot;
72 import com.kitfox.svg.SVGUniverse;
73 import com.kitfox.svg.Text;
74 import com.kitfox.svg.Tspan;
75 import com.kitfox.svg.animation.AnimationElement;
78 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
80 private static final long serialVersionUID = 8508750881358776559L;
82 private static final Logger LOGGER = LoggerFactory.getLogger(SVGNode.class);
83 protected String data = null;
84 protected String defaultData = null;
85 protected Point targetSize = null;
86 protected Boolean useMipMap = true;
87 protected Rectangle2D bounds = null;
89 protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
91 protected transient BufferedImage buffer = null;
92 protected transient String documentCache = null;
93 protected transient SVGDiagram diagramCache = null;
94 protected transient String dataHash = null;
96 static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
104 public void cleanup() {
108 public void setAssignments(List<SVGNodeAssignment> ass) {
110 assignments.addAll(ass);
113 public void cleanDiagramCache() {
114 SVGDiagram d = diagramCache;
117 SVGUniverse univ = SVGCache.getSVGUniverse();
118 if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
120 //System.out.println("cleared: " + d.getXMLBase());
125 static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
127 @PropertySetter("SVG")
129 public void setData(String data) {
130 String cached = dataCache.get(data);
131 if (cached == null) {
133 dataCache.put(data, data);
136 this.defaultData = cached;
140 @SyncField("targetSize")
141 public void setTargetSize(Point p) {
142 this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
145 @SyncField("targetSize")
146 public void setTargetSize(int x, int y) {
147 this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
150 @SyncField("useMipMap")
151 public void useMipMap(Boolean use) {
152 this.useMipMap = use;
155 @PropertySetter("Bounds")
157 public void setBounds(Rectangle2D bounds) {
158 this.bounds = bounds;
162 public Rectangle2D getBoundsInLocal() {
169 public void render(Graphics2D g2d) {
171 return; // Not initialized
173 AffineTransform ot = null;
174 if (!transform.isIdentity()) {
175 ot = g2d.getTransform();
176 g2d.transform(transform);
179 if (g2d.getRenderingHint(G2DRenderingHints.KEY_SVG_PASSTHRU) == Boolean.TRUE) {
180 SVGPassthruShape.resetG2D(g2d);
181 String svg = assignments.isEmpty() ? data : applyAssigments(data, assignments);
183 g2d.fill(new SVGPassthruShape(svg));
186 if (!data.equals(documentCache) || diagramCache == null || buffer == null)
194 g2d.setTransform(ot);
197 protected int dynamicHash() {
201 protected String parseSVG() {
205 SVGUniverse univ = SVGCache.getSVGUniverse();
207 Rectangle2D bbox = null;
208 synchronized (univ) {
209 // Relinquish reference to current element
210 if (diagramCache != null) {
211 univ.decRefCount(diagramCache.getXMLBase());
215 // Lets check for rootAssignment that contributes the whole SVG
216 SVGNodeAssignment rootAssignment = null;
217 if (!assignments.isEmpty()) {
218 for (SVGNodeAssignment ass : assignments) {
219 if (ass.attributeNameOrId.equals("$root")) {
220 rootAssignment = ass;
226 if (rootAssignment != null) {
227 dataBytes = rootAssignment.value.getBytes("UTF-8");
229 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
230 dataBytes = data.getBytes("UTF-8");
232 dataHash = digest(dataBytes, assignments, dynamicHash());
233 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
234 diagramCache = univ.getDiagram(uri, false);
236 if (diagramCache != null) {
237 univ.incRefCount(diagramCache.getXMLBase());
238 SVGRoot root = diagramCache.getRoot();
240 univ.decRefCount(diagramCache.getXMLBase());
241 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
243 univ.incRefCount(diagramCache.getXMLBase());
244 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
246 bbox = root.getBoundingBox();
247 if (bbox.isEmpty()) {
248 // Lets check if this should be visible or not
249 Set<?> presentationAttributes = root.getPresentationAttributes();
250 if (!presentationAttributes.contains("display")) {
251 // TODO: fix this - How can one read values of attributes in SVG salamander???
252 univ.decRefCount(diagramCache.getXMLBase());
253 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
255 univ.incRefCount(diagramCache.getXMLBase());
256 bbox = (Rectangle2D) root.getBoundingBox().clone();
258 bbox = new Rectangle2D.Double(0, 0, 0, 0);
261 if (applyAssignments(diagramCache, assignments)) {
262 bbox = (Rectangle2D) root.getBoundingBox().clone();
264 bbox = (Rectangle2D) bbox.clone();
269 bbox = new Rectangle2D.Double();
273 documentCache = data;
275 } catch (SVGException e) {
276 // This can only occur if diagramCache != null.
277 setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
278 univ.decRefCount(diagramCache.getXMLBase());
280 } catch (IOException e) {
287 private static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
289 dbf.setValidating(false);
291 dbf.setFeature("http://xml.org/sax/features/namespaces", false);
292 dbf.setFeature("http://xml.org/sax/features/validation", false);
293 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
294 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
295 } catch (ParserConfigurationException e) {
300 // Notice: Remember to change both implementations of applyAssigments when modifying the functionality.
301 protected static String applyAssigments(String svg, List<SVGNodeAssignment> assignments) {
303 DocumentBuilder db = dbf.newDocumentBuilder();
304 Document doc = db.parse(new InputSource(new StringReader(svg)));
306 NodeList entries = doc.getElementsByTagName("*");
307 for (int i=0; i<entries.getLength(); i++) {
308 Element element = (Element) entries.item(i);
309 if (element.hasAttribute("id")) {
310 element.setIdAttribute("id", true);
313 for (SVGNodeAssignment ass : assignments) {
314 Element e = doc.getElementById(ass.elementId);
316 if ("$text".equals(ass.attributeNameOrId)) {
317 if (e.getTagName().equals("tspan")) {
318 if (ass.value.trim().isEmpty()) {
319 e.setTextContent("-");
321 e.setTextContent(ass.value);
324 } else if (ass.attributeNameOrId.startsWith("#")) {
325 e.setAttribute(ass.attributeNameOrId.substring(1), ass.value);
327 e.setAttribute(ass.attributeNameOrId, ass.value);
330 LOGGER.warn("Element with id='" + ass.elementId + " was not found.");
334 DOMSource domSource = new DOMSource(doc);
335 StringWriter writer = new StringWriter();
336 StreamResult result = new StreamResult(writer);
337 TransformerFactory tf = TransformerFactory.newInstance();
338 Transformer transformer = tf.newTransformer();
339 transformer.transform(domSource, result);
340 return writer.toString();
342 } catch (Exception e) {
347 // Notice: Remember to change both implementations of applyAssigments when modifying the functionality.
348 protected boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
349 if (assignments.isEmpty())
352 boolean changed = false;
354 // Without this elements are sometimes not found by id!
355 diagram.updateTime(0);
357 for (SVGNodeAssignment ass : assignments) {
358 SVGElement e = diagram.getElement(ass.elementId);
360 if ("$text".equals(ass.attributeNameOrId)) {
361 if (e instanceof Tspan) {
363 if (ass.value.trim().isEmpty()) {
366 t.setText(ass.value);
368 SVGElement parent = t.getParent();
369 if (parent instanceof Text)
370 ((Text) parent).rebuild();
373 } else if (ass.attributeNameOrId.startsWith("#")) {
374 e.setAttribute(ass.attributeNameOrId.substring(1), AnimationElement.AT_CSS, ass.value);
377 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
383 // Without this the attribute values are not correctly reflected in rendering
384 diagram.updateTime(0);
389 public static Rectangle2D getBounds(String data) {
390 return getBounds(data, 0);
393 public static Rectangle2D getBounds(String data, int dynamicHash) {
394 return getBounds(data, Collections.emptyList(), dynamicHash);
397 public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
399 new Exception("null SVG data").printStackTrace();
403 SVGDiagram diagramCache = null;
405 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
406 byte[] dataBytes = data.getBytes("UTF-8");
407 String digest = digest(dataBytes, assignments, dynamicHash);
409 SVGUniverse univ = SVGCache.getSVGUniverse();
410 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
411 synchronized (univ) {
412 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
413 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
414 diagramCache = univ.getDiagram(uri, false);
415 if (diagramCache != null) {
416 if (diagramCache.getRoot() == null) {
417 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
418 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
419 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
424 Rectangle2D rect = null;
425 if (diagramCache != null) {
426 SVGRoot root = diagramCache.getRoot();
427 Rectangle2D bbox = root.getBoundingBox();
428 rect = (Rectangle2D) bbox.clone();
430 rect = new Rectangle2D.Double();
433 } catch (SVGException e) {
434 return diagramCache.getViewRect(new Rectangle2D.Double());
435 } catch(IOException e) {
440 public static Rectangle2D getRealBounds(String data) {
441 return getRealBounds(data, Collections.emptyList(), 0);
444 public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
446 new Exception("null SVG data").printStackTrace();
450 SVGDiagram diagramCache = null;
452 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
453 byte[] dataBytes = data.getBytes("UTF-8");
454 String digest = digest(dataBytes, assignments, dynamicHash);
456 SVGUniverse univ = SVGCache.getSVGUniverse();
457 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
458 synchronized (univ) {
459 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
460 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
461 diagramCache = univ.getDiagram(uri, false);
462 if (diagramCache != null) {
463 SVGRoot root = diagramCache.getRoot();
464 if (root == null) return new Rectangle2D.Double();
465 return (Rectangle2D)root.getBoundingBox().clone();
468 } catch (SVGException e) {
469 return diagramCache.getViewRect(new Rectangle2D.Double());
470 } catch(IOException e) {
475 protected void initBuffer(Graphics2D g2d) {
476 if (!data.equals(documentCache) || diagramCache == null) {
477 dataHash = parseSVG();
478 if (diagramCache == null) {
479 LOGGER.warn("UNABLE TO PARSE SVG:\n" + data);
484 if (buffer != null) {
487 diagramCache.setIgnoringClipHeuristic(true); // FIXME
488 if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
489 buffer = bufferCache.get(dataHash).get();
490 } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
492 } else if(useMipMap) {
493 if(G2DUtils.isAccelerated(g2d)) {
494 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
496 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
498 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
500 if(G2DUtils.isAccelerated(g2d)) {
501 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
503 buffer = new BufferedImage(diagramCache, bounds, targetSize);
505 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
509 public void setProperty(String field, Object value) {
510 if("data".equals(field)) {
511 // System.out.println("SVGNode data -> " + value);
512 this.data = (String)value;
513 } else if ("z".equals(field)) {
514 // System.out.println("SVGNode z -> " + value);
515 setZIndex((Integer)value);
516 } else if ("position".equals(field)) {
517 // System.out.println("SVGNode position -> " + value);
518 Point point = (Point)value;
519 setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
520 // setPosition(point.x, point.y);
525 public void initValues() {
531 static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
533 static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments, int dynamicHash) {
535 MessageDigest md = MessageDigest.getInstance("MD5");
536 byte[] messageDigest = md.digest(dataBytes);
537 BigInteger number = new BigInteger(1, messageDigest);
538 String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0) + 31 * dynamicHash;
539 String result = digestCache.get(dataHash);
542 digestCache.put(dataHash,dataHash);
545 } catch (NoSuchAlgorithmException e) {
547 throw new Error("MD5 digest must exist.");
551 static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
552 static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
555 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
556 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
560 public <T> T getProperty(String propertyName) {
565 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
568 public void synchronizeDocument(String document) {
572 public void synchronizeTransform(double[] data) {
573 this.setTransform(new AffineTransform(data));
576 public String getSVGText() {
577 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
578 //return diagramCache.toString();
579 //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>");
583 public Rectangle2D getElementBounds(String id) throws SVGException {
584 SVGElement e = diagramCache.getElement(id);
585 if (e instanceof RenderableElement) {
586 return ((RenderableElement)e).getBoundingBox();