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