]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Support SVG generation from scenegraph
[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                         t.setText(ass.value);
282                         SVGElement parent = t.getParent();
283                         if (parent instanceof Text)
284                             ((Text) parent).rebuild();
285                         changed = true;
286                     }
287                 } else {
288                     e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
289                     changed = true;
290                 }
291             }
292         }
293         diagram.updateTime(0);
294         return changed;
295     }
296
297     public static Rectangle2D getBounds(String data) {
298         return getBounds(data, null);
299     }
300
301     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
302         if (data == null) {
303             new Exception("null SVG data").printStackTrace();
304             return null;
305         }
306
307         SVGDiagram diagramCache = null;
308         try {
309             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
310             byte[] dataBytes = data.getBytes("UTF-8");
311             String digest = digest(dataBytes, assignments);
312
313             SVGUniverse univ = SVGCache.getSVGUniverse();
314             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
315             synchronized (univ) {
316                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
317                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
318                 diagramCache = univ.getDiagram(uri, false);
319                 if (diagramCache != null) {
320                     if (diagramCache.getRoot() == null) {
321                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
322                     } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
323                         diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
324                     }
325                 }
326             }
327
328             Rectangle2D rect = null;
329             if (diagramCache != null) {
330                 SVGRoot root = diagramCache.getRoot();
331                 Rectangle2D bbox = root.getBoundingBox();
332                 rect = (Rectangle2D) bbox.clone();
333             } else {
334                 rect = new Rectangle2D.Double();
335             }
336             return rect;
337         } catch (SVGException e) {
338             return diagramCache.getViewRect(new Rectangle2D.Double());
339         } catch(IOException e) {
340         }
341         return null;
342     }
343
344     public static Rectangle2D getRealBounds(String data) {
345         return getRealBounds(data, null);
346     }
347
348     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
349         if (data == null) {
350             new Exception("null SVG data").printStackTrace();
351             return null;
352         }
353
354         SVGDiagram diagramCache = null;
355         try {
356             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
357             byte[] dataBytes = data.getBytes("UTF-8");
358             String digest = digest(dataBytes, assignments);
359
360             SVGUniverse univ = SVGCache.getSVGUniverse();
361             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
362             synchronized (univ) {
363                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
364                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
365                 diagramCache = univ.getDiagram(uri, false);
366                 if (diagramCache != null) {
367                     SVGRoot root = diagramCache.getRoot(); 
368                     if (root == null) return new Rectangle2D.Double();
369                     return (Rectangle2D)root.getBoundingBox().clone();
370                 }
371             }
372         } catch (SVGException e) {
373             return diagramCache.getViewRect(new Rectangle2D.Double());
374         } catch(IOException e) {
375         }
376         return null;
377     }
378
379     protected void initBuffer(Graphics2D g2d) {
380         
381         if (!data.equals(documentCache) || diagramCache == null) {
382                 dataHash = parseSVG();
383                 if (diagramCache == null) {
384                         System.err.println("UNABLE TO PARSE SVG:\n" + data);
385                         return;
386                 }
387         }
388
389         if (buffer != null) {
390             buffer = null;
391         }
392         diagramCache.setIgnoringClipHeuristic(true); // FIXME
393         if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
394             buffer = bufferCache.get(dataHash).get();
395         } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
396             buffer = null;
397         } else if(useMipMap) {
398                 if(G2DUtils.isAccelerated(g2d)) {
399                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
400             } else {
401                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
402             }
403             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
404         } else {
405                 if(G2DUtils.isAccelerated(g2d)) {
406                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
407             } else {
408                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
409             }
410             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
411         }
412     }
413
414     public void setProperty(String field, Object value) {
415         if("data".equals(field)) {
416 //              System.out.println("SVGNode data -> " + value);
417             this.data = (String)value;
418         } else if ("z".equals(field)) {
419 //              System.out.println("SVGNode z -> " + value);
420             setZIndex((Integer)value);
421         } else if ("position".equals(field)) {
422 //              System.out.println("SVGNode position -> " + value);
423             Point point = (Point)value;
424             setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
425 //              setPosition(point.x, point.y);
426         }
427     }
428
429     @Override
430     public void initValues() {
431         data = defaultData;
432         dataHash = null;
433         buffer =  null;
434     }
435
436     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
437     
438     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
439         try {
440             MessageDigest md = MessageDigest.getInstance("MD5");
441             byte[] messageDigest = md.digest(dataBytes);
442             BigInteger number = new BigInteger(1, messageDigest);
443             String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);
444             String result = digestCache.get(dataHash);
445             if(result == null) {
446                 result = dataHash;
447                 digestCache.put(dataHash,dataHash);
448             }
449             return result;
450         } catch (NoSuchAlgorithmException e) {
451             // Shouldn't happen
452             throw new Error("MD5 digest must exist.");
453         }
454     }
455
456     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
457     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
458
459         @Override
460         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
461                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
462         }
463         
464         @Override
465         public <T> T getProperty(String propertyName) {
466                 return null;
467         }
468
469         @Override
470         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
471         }
472
473         public void synchronizeDocument(String document) {
474                 setData(document);
475         }
476
477         public void synchronizeTransform(double[] data) {
478                 this.setTransform(new AffineTransform(data));
479         }
480         
481         public String getSVGText() {
482                 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
483                 //return diagramCache.toString();
484                 //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>");
485                 return ret;
486         }
487         
488 }