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