]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Merge "(refs #6878) Don't validate SCL expressions in console input area"
[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.lang.ref.WeakReference;
21 import java.math.BigInteger;
22 import java.net.URI;
23 import java.net.URL;
24 import java.security.MessageDigest;
25 import java.security.NoSuchAlgorithmException;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.WeakHashMap;
31
32 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
33 import org.simantics.scenegraph.LoaderNode;
34 import org.simantics.scenegraph.ScenegraphUtils;
35 import org.simantics.scenegraph.g2d.G2DNode;
36 import org.simantics.scenegraph.utils.BufferedImage;
37 import org.simantics.scenegraph.utils.G2DUtils;
38 import org.simantics.scenegraph.utils.InitValueSupport;
39 import org.simantics.scenegraph.utils.MipMapBufferedImage;
40 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
41 import org.simantics.scenegraph.utils.VRamBufferedImage;
42 import org.simantics.scl.runtime.function.Function1;
43 import org.simantics.scl.runtime.function.Function2;
44 import org.simantics.utils.threads.AWTThread;
45
46 import com.kitfox.svg.SVGCache;
47 import com.kitfox.svg.SVGDiagram;
48 import com.kitfox.svg.SVGElement;
49 import com.kitfox.svg.SVGException;
50 import com.kitfox.svg.SVGRoot;
51 import com.kitfox.svg.SVGUniverse;
52 import com.kitfox.svg.Text;
53 import com.kitfox.svg.Tspan;
54 import com.kitfox.svg.animation.AnimationElement;
55
56 @RasterOutputWidget
57 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
58
59         public static class SVGNodeAssignment {
60                 public String elementId;
61                 public String attributeNameOrId;
62                 public String value;
63                 public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) {
64                         this.elementId = elementId;
65                         this.attributeNameOrId = attributeNameOrId;
66                         this.value = value;
67                 }
68                 @Override
69                 public int hashCode() {
70                         final int prime = 31;
71                         int result = 1;
72                         result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode());
73                         result = prime * result + ((elementId == null) ? 0 : elementId.hashCode());
74                         result = prime * result + ((value == null) ? 0 : value.hashCode());
75                         return result;
76                 }
77                 @Override
78                 public boolean equals(Object obj) {
79                         if (this == obj)
80                                 return true;
81                         if (obj == null)
82                                 return false;
83                         if (getClass() != obj.getClass())
84                                 return false;
85                         SVGNodeAssignment other = (SVGNodeAssignment) obj;
86                         if (attributeNameOrId == null) {
87                                 if (other.attributeNameOrId != null)
88                                         return false;
89                         } else if (!attributeNameOrId.equals(other.attributeNameOrId))
90                                 return false;
91                         if (elementId == null) {
92                                 if (other.elementId != null)
93                                         return false;
94                         } else if (!elementId.equals(other.elementId))
95                                 return false;
96                         if (value == null) {
97                                 if (other.value != null)
98                                         return false;
99                         } else if (!value.equals(other.value))
100                                 return false;
101                         return true;
102                 }
103         }
104         
105     private static final long serialVersionUID = 8508750881358776559L;
106
107     protected String          data             = null;
108     protected String          defaultData      = null;
109     protected Point           targetSize       = null;
110     protected Boolean         useMipMap        = true;
111     protected Rectangle2D     bounds           = null;
112     
113     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
114
115     transient BufferedImage buffer    = null;
116     transient String documentCache    = null;
117     transient SVGDiagram diagramCache = null;
118     transient String dataHash         = null;
119
120     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
121
122     @Override
123     public void cleanup() {
124         cleanDiagramCache();
125     }
126
127     public void setAssignments(List<SVGNodeAssignment> ass) {
128         assignments.clear();
129         assignments.addAll(ass);
130     }
131     
132     public void cleanDiagramCache() {
133         SVGDiagram d = diagramCache;
134         if (d != null) {
135             diagramCache = null;
136             SVGUniverse univ = SVGCache.getSVGUniverse();
137             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
138                 // Cleared!
139                 //System.out.println("cleared: " + d.getXMLBase());
140             }
141         }
142     }
143
144     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
145
146     @PropertySetter("SVG")
147     @SyncField("data")
148     public void setData(String data) {
149         String cached = dataCache.get(data);
150         if (cached == null) {
151             cached = data;
152             dataCache.put(data, data);
153         }
154         this.data = cached;
155         this.defaultData = cached;
156     }
157
158     @SyncField("targetSize")
159     public void setTargetSize(Point p) {
160         this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
161     }
162
163     @SyncField("targetSize")
164     public void setTargetSize(int x, int y) {
165         this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
166     }
167
168     @SyncField("useMipMap")
169     public void useMipMap(Boolean use) {
170         this.useMipMap = use;
171     }
172
173     @PropertySetter("Bounds")
174     @SyncField("bounds")
175     public void setBounds(Rectangle2D bounds) {
176         this.bounds = bounds;
177     }
178
179     @Override
180     public Rectangle2D getBoundsInLocal() {
181         if (bounds == null)
182             parseSVG();
183         return bounds;
184     }
185
186     @Override
187     public void render(Graphics2D g2d) {
188         if (data == null)
189             return; // Not initialized
190
191         if (!data.equals(documentCache) || diagramCache == null || buffer == null)
192             initBuffer(g2d);
193
194         AffineTransform ot = null;
195         if (!transform.isIdentity()) {
196             ot = g2d.getTransform();
197             g2d.transform(transform);
198         }
199
200         if (buffer != null)
201             buffer.paint(g2d);
202
203         if (ot != null)
204             g2d.setTransform(ot);
205     }
206
207     protected String parseSVG() {
208         if (data == null)
209             return null;
210
211         SVGUniverse univ = SVGCache.getSVGUniverse();
212         try {
213             Rectangle2D bbox = null;
214             synchronized (univ) {
215                 // Relinquish reference to current element
216                 if (diagramCache != null) {
217                     univ.decRefCount(diagramCache.getXMLBase());
218                     diagramCache = null;
219                 }
220
221                 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
222                 byte[] dataBytes = data.getBytes("UTF-8");
223                 dataHash = digest(dataBytes, assignments);
224                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
225                 diagramCache = univ.getDiagram(uri, false);
226
227                 if (diagramCache != null) {
228                     univ.incRefCount(diagramCache.getXMLBase());
229
230                     if (diagramCache.getRoot() == null) {
231                         univ.decRefCount(diagramCache.getXMLBase());
232                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
233                         dataHash = "broken";
234                         univ.incRefCount(diagramCache.getXMLBase());
235                         bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
236                     } else {
237                         bbox = diagramCache.getRoot().getBoundingBox();
238                         if (bbox.isEmpty()) {
239                             univ.decRefCount(diagramCache.getXMLBase());
240                             diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
241                             dataHash = "empty";
242                             univ.incRefCount(diagramCache.getXMLBase());
243                             bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
244                         } else {
245                             if (applyAssignments(diagramCache, assignments)) {
246                                 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
247                             } else {
248                                 bbox = (Rectangle2D) bbox.clone();
249                             }
250                         }
251                     }
252                 } else {
253                     bbox = new Rectangle2D.Double();
254                 }
255             }
256
257             documentCache = data;
258             setBounds(bbox);
259         } catch (SVGException e) {
260             // This can only occur if diagramCache != null.
261             setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
262             univ.decRefCount(diagramCache.getXMLBase());
263             diagramCache = null;
264         } catch (IOException e) {
265             diagramCache = null;
266         }
267
268         return dataHash;
269     }
270
271     private static boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
272         if (assignments.isEmpty())
273             return false;
274         boolean changed = false;
275         for (SVGNodeAssignment ass : assignments) {
276             SVGElement e = diagram.getElement(ass.elementId);
277             if (e != null) {
278                 if ("$text".equals(ass.attributeNameOrId)) {
279                     if (e instanceof Tspan) {
280                         Tspan t = (Tspan) e;
281                         if (ass.value.trim().isEmpty()) {
282                                 t.setText("-");
283                         } else {
284                                 t.setText(ass.value);
285                         }
286                         SVGElement parent = t.getParent();
287                         if (parent instanceof Text)
288                             ((Text) parent).rebuild();
289                         changed = true;
290                     }
291                 } else {
292                     e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
293                     changed = true;
294                 }
295             }
296         }
297         diagram.updateTime(0);
298         return changed;
299     }
300
301     public static Rectangle2D getBounds(String data) {
302         return getBounds(data, null);
303     }
304
305     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
306         if (data == null) {
307             new Exception("null SVG data").printStackTrace();
308             return null;
309         }
310
311         SVGDiagram diagramCache = null;
312         try {
313             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
314             byte[] dataBytes = data.getBytes("UTF-8");
315             String digest = digest(dataBytes, assignments);
316
317             SVGUniverse univ = SVGCache.getSVGUniverse();
318             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
319             synchronized (univ) {
320                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
321                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
322                 diagramCache = univ.getDiagram(uri, false);
323                 if (diagramCache != null) {
324                     if (diagramCache.getRoot() == null) {
325                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
326                     } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
327                         diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
328                     }
329                 }
330             }
331
332             Rectangle2D rect = null;
333             if (diagramCache != null) {
334                 SVGRoot root = diagramCache.getRoot();
335                 Rectangle2D bbox = root.getBoundingBox();
336                 rect = (Rectangle2D) bbox.clone();
337             } else {
338                 rect = new Rectangle2D.Double();
339             }
340             return rect;
341         } catch (SVGException e) {
342             return diagramCache.getViewRect(new Rectangle2D.Double());
343         } catch(IOException e) {
344         }
345         return null;
346     }
347
348     public static Rectangle2D getRealBounds(String data) {
349         return getRealBounds(data, null);
350     }
351
352     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
353         if (data == null) {
354             new Exception("null SVG data").printStackTrace();
355             return null;
356         }
357
358         SVGDiagram diagramCache = null;
359         try {
360             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
361             byte[] dataBytes = data.getBytes("UTF-8");
362             String digest = digest(dataBytes, assignments);
363
364             SVGUniverse univ = SVGCache.getSVGUniverse();
365             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
366             synchronized (univ) {
367                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
368                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
369                 diagramCache = univ.getDiagram(uri, false);
370                 if (diagramCache != null) {
371                     SVGRoot root = diagramCache.getRoot(); 
372                     if (root == null) return new Rectangle2D.Double();
373                     return (Rectangle2D)root.getBoundingBox().clone();
374                 }
375             }
376         } catch (SVGException e) {
377             return diagramCache.getViewRect(new Rectangle2D.Double());
378         } catch(IOException e) {
379         }
380         return null;
381     }
382
383     protected void initBuffer(Graphics2D g2d) {
384         
385         if (!data.equals(documentCache) || diagramCache == null) {
386                 dataHash = parseSVG();
387                 if (diagramCache == null) {
388                         System.err.println("UNABLE TO PARSE SVG:\n" + data);
389                         return;
390                 }
391         }
392
393         if (buffer != null) {
394             buffer = null;
395         }
396         diagramCache.setIgnoringClipHeuristic(true); // FIXME
397         if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
398             buffer = bufferCache.get(dataHash).get();
399         } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
400             buffer = null;
401         } else if(useMipMap) {
402                 if(G2DUtils.isAccelerated(g2d)) {
403                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
404             } else {
405                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
406             }
407             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
408         } else {
409                 if(G2DUtils.isAccelerated(g2d)) {
410                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
411             } else {
412                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
413             }
414             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
415         }
416     }
417
418     public void setProperty(String field, Object value) {
419         if("data".equals(field)) {
420 //              System.out.println("SVGNode data -> " + value);
421             this.data = (String)value;
422         } else if ("z".equals(field)) {
423 //              System.out.println("SVGNode z -> " + value);
424             setZIndex((Integer)value);
425         } else if ("position".equals(field)) {
426 //              System.out.println("SVGNode position -> " + value);
427             Point point = (Point)value;
428             setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
429 //              setPosition(point.x, point.y);
430         }
431     }
432
433     @Override
434     public void initValues() {
435         data = defaultData;
436         dataHash = null;
437         buffer =  null;
438     }
439
440     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
441     
442     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
443         try {
444             MessageDigest md = MessageDigest.getInstance("MD5");
445             byte[] messageDigest = md.digest(dataBytes);
446             BigInteger number = new BigInteger(1, messageDigest);
447             String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);
448             String result = digestCache.get(dataHash);
449             if(result == null) {
450                 result = dataHash;
451                 digestCache.put(dataHash,dataHash);
452             }
453             return result;
454         } catch (NoSuchAlgorithmException e) {
455             // Shouldn't happen
456             throw new Error("MD5 digest must exist.");
457         }
458     }
459
460     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
461     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
462
463         @Override
464         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
465                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
466         }
467         
468         @Override
469         public <T> T getProperty(String propertyName) {
470                 return null;
471         }
472
473         @Override
474         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
475         }
476
477         public void synchronizeDocument(String document) {
478                 setData(document);
479         }
480
481         public void synchronizeTransform(double[] data) {
482                 this.setTransform(new AffineTransform(data));
483         }
484         
485         public String getSVGText() {
486                 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
487                 //return diagramCache.toString();
488                 //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>");
489                 return ret;
490         }
491         
492 }