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