]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
6fd25e6f8359cbf81dfe7922448d837915618a18
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / SVGNode.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.scenegraph.g2d.nodes;
13
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;
24 import java.net.URI;
25 import java.net.URL;
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;
32 import java.util.Map;
33 import java.util.Set;
34 import java.util.WeakHashMap;
35
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;
43
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.w3c.dom.Document;
60 import org.w3c.dom.Element;
61 import org.w3c.dom.NodeList;
62 import org.xml.sax.InputSource;
63
64 import com.kitfox.svg.RenderableElement;
65 import com.kitfox.svg.SVGCache;
66 import com.kitfox.svg.SVGDiagram;
67 import com.kitfox.svg.SVGElement;
68 import com.kitfox.svg.SVGException;
69 import com.kitfox.svg.SVGRoot;
70 import com.kitfox.svg.SVGUniverse;
71 import com.kitfox.svg.Text;
72 import com.kitfox.svg.Tspan;
73 import com.kitfox.svg.animation.AnimationElement;
74
75 @RasterOutputWidget
76 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
77
78         private static final long serialVersionUID = 8508750881358776559L;
79
80     protected String          data             = null;
81     protected String          defaultData      = null;
82     protected Point           targetSize       = null;
83     protected Boolean         useMipMap        = true;
84     protected Rectangle2D     bounds           = null;
85
86     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
87
88     protected transient BufferedImage buffer    = null;
89     protected transient String documentCache    = null;
90     protected transient SVGDiagram diagramCache = null;
91     protected transient String dataHash         = null;
92
93     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
94
95     @Override
96     public void init() {
97         super.init();
98     }
99
100     @Override
101     public void cleanup() {
102         cleanDiagramCache();
103     }
104
105     public void setAssignments(List<SVGNodeAssignment> ass) {
106         assignments.clear();
107         assignments.addAll(ass);
108     }
109     
110     public void cleanDiagramCache() {
111         SVGDiagram d = diagramCache;
112         if (d != null) {
113             diagramCache = null;
114             SVGUniverse univ = SVGCache.getSVGUniverse();
115             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
116                 // Cleared!
117                 //System.out.println("cleared: " + d.getXMLBase());
118             }
119         }
120     }
121
122     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
123
124     @PropertySetter("SVG")
125     @SyncField("data")
126     public void setData(String data) {
127         String cached = dataCache.get(data);
128         if (cached == null) {
129             cached = data;
130             dataCache.put(data, data);
131         }
132         this.data = cached;
133         this.defaultData = cached;
134         cleanDiagramCache();
135     }
136
137     @SyncField("targetSize")
138     public void setTargetSize(Point p) {
139         this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
140     }
141
142     @SyncField("targetSize")
143     public void setTargetSize(int x, int y) {
144         this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
145     }
146
147     @SyncField("useMipMap")
148     public void useMipMap(Boolean use) {
149         this.useMipMap = use;
150     }
151
152     @PropertySetter("Bounds")
153     @SyncField("bounds")
154     public void setBounds(Rectangle2D bounds) {
155         this.bounds = bounds;
156     }
157
158     @Override
159     public Rectangle2D getBoundsInLocal() {
160         if (bounds == null)
161             parseSVG();
162         return bounds;
163     }
164
165     @Override
166     public void render(Graphics2D g2d) {
167         if (data == null)
168             return; // Not initialized
169
170         AffineTransform ot = null;
171         if (!transform.isIdentity()) {
172             ot = g2d.getTransform();
173             g2d.transform(transform);
174         }
175
176         if (g2d.getRenderingHint(G2DRenderingHints.KEY_SVG_PASSTHRU) == Boolean.TRUE) {
177             SVGPassthruShape.resetG2D(g2d);
178             String svg = assignments.isEmpty() ? data : applyAssigments(data, assignments);
179             if (svg != null) {
180                 g2d.fill(new SVGPassthruShape(svg));
181             }
182         } else {
183             if (!data.equals(documentCache) || diagramCache == null || buffer == null)
184                 initBuffer(g2d);
185
186             if (buffer != null)
187                 buffer.paint(g2d);
188         }
189
190         if (ot != null)
191             g2d.setTransform(ot);
192     }
193
194     protected int dynamicHash() {
195         return 0;
196     }
197
198     protected String parseSVG() {
199         if (data == null)
200             return null;
201
202         SVGUniverse univ = SVGCache.getSVGUniverse();
203         try {
204             Rectangle2D bbox = null;
205             synchronized (univ) {
206                 // Relinquish reference to current element
207                 if (diagramCache != null) {
208                     univ.decRefCount(diagramCache.getXMLBase());
209                     diagramCache = null;
210                 }
211
212                 // Lets check for rootAssignment that contributes the whole SVG
213                 SVGNodeAssignment rootAssignment = null;
214                 if (!assignments.isEmpty()) {
215                     for (SVGNodeAssignment ass : assignments) {
216                         if (ass.attributeNameOrId.equals("$root")) {
217                             rootAssignment = ass;
218                             break;
219                         }
220                     }
221                 }
222                 byte[] dataBytes;
223                 if (rootAssignment != null) {
224                     dataBytes = rootAssignment.value.getBytes("UTF-8");
225                 } else {
226                     // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
227                     dataBytes = data.getBytes("UTF-8");
228                 }
229                 dataHash = digest(dataBytes, assignments, dynamicHash());
230                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
231                 diagramCache = univ.getDiagram(uri, false);
232
233                 if (diagramCache != null) {
234                     univ.incRefCount(diagramCache.getXMLBase());
235                     SVGRoot root = diagramCache.getRoot();
236                     if (root == null) {
237                         univ.decRefCount(diagramCache.getXMLBase());
238                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
239                         dataHash = "broken";
240                         univ.incRefCount(diagramCache.getXMLBase());
241                         bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
242                     } else {
243                         bbox = root.getBoundingBox();
244                         if (bbox.isEmpty()) {
245                             // Lets check if this should be visible or not
246                             Set<?> presentationAttributes = root.getPresentationAttributes();
247                             if (!presentationAttributes.contains("display")) {
248                                 // TODO: fix this - How can one read values of attributes in SVG salamander???
249                                 univ.decRefCount(diagramCache.getXMLBase());
250                                 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
251                                 dataHash = "empty";
252                                 univ.incRefCount(diagramCache.getXMLBase());
253                                 bbox = (Rectangle2D) root.getBoundingBox().clone();
254                             } else {
255                                 bbox = new Rectangle2D.Double(0, 0, 0, 0);
256                             }
257                         } else {
258                             if (applyAssignments(diagramCache, assignments)) {
259                                 bbox = (Rectangle2D) root.getBoundingBox().clone();
260                             } else {
261                                 bbox = (Rectangle2D) bbox.clone();
262                             }
263                         }
264                     }
265                 } else {
266                     bbox = new Rectangle2D.Double();
267                 }
268             }
269
270             documentCache = data;
271             setBounds(bbox);
272         } catch (SVGException e) {
273             // This can only occur if diagramCache != null.
274             setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
275             univ.decRefCount(diagramCache.getXMLBase());
276             diagramCache = null;
277         } catch (IOException e) {
278             diagramCache = null;
279         }
280
281         return dataHash;
282     }
283
284     private static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
285     static {
286         dbf.setValidating(false);
287         try {
288             dbf.setFeature("http://xml.org/sax/features/namespaces", false);
289             dbf.setFeature("http://xml.org/sax/features/validation", false);
290             dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
291             dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
292         } catch (ParserConfigurationException e) {
293             // Nothing to do
294         }
295     }
296
297     // Notice: Remember to change both implementations of applyAssigments when modifying the functionality.
298     protected static String applyAssigments(String svg, List<SVGNodeAssignment> assignments) {
299         try {
300             DocumentBuilder db = dbf.newDocumentBuilder();
301             Document doc = db.parse(new InputSource(new StringReader(svg)));
302
303             NodeList entries = doc.getElementsByTagName("*");
304             for (int i=0; i<entries.getLength(); i++) {
305                 Element element = (Element) entries.item(i);
306                 if (element.hasAttribute("id")) {
307                     element.setIdAttribute("id", true);
308                 }
309             }
310             for (SVGNodeAssignment ass : assignments) {
311                 Element e = doc.getElementById(ass.elementId);
312                 if (e != null) {
313                     if ("$text".equals(ass.attributeNameOrId)) {
314                         if (e.getTagName().equals("tspan")) {
315                             if (ass.value.trim().isEmpty()) {
316                                 e.setTextContent("-");
317                             } else {
318                                 e.setTextContent(ass.value);
319                             }
320                         }
321                     } else if (ass.attributeNameOrId.startsWith("#")) {
322                         e.setAttribute(ass.attributeNameOrId.substring(1), ass.value);
323                     } else {
324                         e.setAttribute(ass.attributeNameOrId, ass.value);
325                     }
326                 } else {
327                     System.err.println("Element with id='" + ass.elementId + " was not found.");
328                 }
329             }
330
331             DOMSource domSource = new DOMSource(doc);
332             StringWriter writer = new StringWriter();
333             StreamResult result = new StreamResult(writer);
334             TransformerFactory tf = TransformerFactory.newInstance();
335             Transformer transformer = tf.newTransformer();
336             transformer.transform(domSource, result);
337             return writer.toString();
338
339         } catch (Exception e) {
340             return null;
341         }
342     }
343
344     // Notice: Remember to change both implementations of applyAssigments when modifying the functionality.
345     protected boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
346         if (assignments.isEmpty())
347             return false;
348
349         boolean changed = false;
350
351         // Without this elements are sometimes not found by id!
352         diagram.updateTime(0);
353
354         for (SVGNodeAssignment ass : assignments) {
355             SVGElement e = diagram.getElement(ass.elementId);
356             if (e != null) {
357                 if ("$text".equals(ass.attributeNameOrId)) {
358                     if (e instanceof Tspan) {
359                         Tspan t = (Tspan) e;
360                         if (ass.value.trim().isEmpty()) {
361                                 t.setText("-");
362                         } else {
363                                 t.setText(ass.value);
364                         }
365                         SVGElement parent = t.getParent();
366                         if (parent instanceof Text)
367                             ((Text) parent).rebuild();
368                         changed = true;
369                     }
370                 } else if (ass.attributeNameOrId.startsWith("#")) {
371                     e.setAttribute(ass.attributeNameOrId.substring(1), AnimationElement.AT_CSS, ass.value);
372                     changed = true;
373                 } else {
374                     e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
375                     changed = true;
376                 }
377             }
378         }
379
380         // Without this the attribute values are not correctly reflected in rendering
381         diagram.updateTime(0);
382
383         return changed;
384     }
385
386     public static Rectangle2D getBounds(String data) {
387         return getBounds(data, 0);
388     }
389
390     public static Rectangle2D getBounds(String data, int dynamicHash) {
391         return getBounds(data, Collections.emptyList(), dynamicHash);
392     }
393
394     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
395         if (data == null) {
396             new Exception("null SVG data").printStackTrace();
397             return null;
398         }
399
400         SVGDiagram diagramCache = null;
401         try {
402             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
403             byte[] dataBytes = data.getBytes("UTF-8");
404             String digest = digest(dataBytes, assignments, dynamicHash);
405
406             SVGUniverse univ = SVGCache.getSVGUniverse();
407             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
408             synchronized (univ) {
409                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
410                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
411                 diagramCache = univ.getDiagram(uri, false);
412                 if (diagramCache != null) {
413                     if (diagramCache.getRoot() == null) {
414                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
415                     } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
416                         diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
417                     }
418                 }
419             }
420
421             Rectangle2D rect = null;
422             if (diagramCache != null) {
423                 SVGRoot root = diagramCache.getRoot();
424                 Rectangle2D bbox = root.getBoundingBox();
425                 rect = (Rectangle2D) bbox.clone();
426             } else {
427                 rect = new Rectangle2D.Double();
428             }
429             return rect;
430         } catch (SVGException e) {
431             return diagramCache.getViewRect(new Rectangle2D.Double());
432         } catch(IOException e) {
433         }
434         return null;
435     }
436
437     public static Rectangle2D getRealBounds(String data) {
438         return getRealBounds(data, Collections.emptyList(), 0);
439     }
440
441     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
442         if (data == null) {
443             new Exception("null SVG data").printStackTrace();
444             return null;
445         }
446
447         SVGDiagram diagramCache = null;
448         try {
449             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
450             byte[] dataBytes = data.getBytes("UTF-8");
451             String digest = digest(dataBytes, assignments, dynamicHash);
452
453             SVGUniverse univ = SVGCache.getSVGUniverse();
454             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
455             synchronized (univ) {
456                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
457                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
458                 diagramCache = univ.getDiagram(uri, false);
459                 if (diagramCache != null) {
460                     SVGRoot root = diagramCache.getRoot();
461                     if (root == null) return new Rectangle2D.Double();
462                     return (Rectangle2D)root.getBoundingBox().clone();
463                 }
464             }
465         } catch (SVGException e) {
466             return diagramCache.getViewRect(new Rectangle2D.Double());
467         } catch(IOException e) {
468         }
469         return null;
470     }
471
472     protected void initBuffer(Graphics2D g2d) {
473         if (!data.equals(documentCache) || diagramCache == null) {
474             dataHash = parseSVG();
475             if (diagramCache == null) {
476                 System.err.println("UNABLE TO PARSE SVG:\n" + data);
477                 return;
478             }
479         }
480
481         if (buffer != null) {
482             buffer = null;
483         }
484         diagramCache.setIgnoringClipHeuristic(true); // FIXME
485         if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
486             buffer = bufferCache.get(dataHash).get();
487         } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
488             buffer = null;
489         } else if(useMipMap) {
490             if(G2DUtils.isAccelerated(g2d)) {
491                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
492             } else {
493                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
494             }
495             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
496         } else {
497             if(G2DUtils.isAccelerated(g2d)) {
498                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
499             } else {
500                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
501             }
502             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
503         }
504     }
505
506     public void setProperty(String field, Object value) {
507         if("data".equals(field)) {
508 //              System.out.println("SVGNode data -> " + value);
509             this.data = (String)value;
510         } else if ("z".equals(field)) {
511 //              System.out.println("SVGNode z -> " + value);
512             setZIndex((Integer)value);
513         } else if ("position".equals(field)) {
514 //              System.out.println("SVGNode position -> " + value);
515             Point point = (Point)value;
516             setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
517 //              setPosition(point.x, point.y);
518         }
519     }
520
521     @Override
522     public void initValues() {
523         data = defaultData;
524         dataHash = null;
525         buffer =  null;
526     }
527
528     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
529
530     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments, int dynamicHash) {
531         try {
532             MessageDigest md = MessageDigest.getInstance("MD5");
533             byte[] messageDigest = md.digest(dataBytes);
534             BigInteger number = new BigInteger(1, messageDigest);
535             String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0) + 31 * dynamicHash;
536             String result = digestCache.get(dataHash);
537             if(result == null) {
538                 result = dataHash;
539                 digestCache.put(dataHash,dataHash);
540             }
541             return result;
542         } catch (NoSuchAlgorithmException e) {
543             // Shouldn't happen
544             throw new Error("MD5 digest must exist.");
545         }
546     }
547
548     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
549     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
550
551         @Override
552         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
553                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
554         }
555         
556         @Override
557         public <T> T getProperty(String propertyName) {
558                 return null;
559         }
560
561         @Override
562         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
563         }
564
565         public void synchronizeDocument(String document) {
566                 setData(document);
567         }
568
569         public void synchronizeTransform(double[] data) {
570                 this.setTransform(new AffineTransform(data));
571         }
572
573         public String getSVGText() {
574                 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
575                 //return diagramCache.toString();
576                 //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>");
577                 return ret;
578         }
579
580     public Rectangle2D getElementBounds(String id) throws SVGException {
581         SVGElement e = diagramCache.getElement(id);
582         if (e instanceof RenderableElement) {
583             return ((RenderableElement)e).getBoundingBox();
584         } else {
585            return null;
586         }
587     }
588
589 }