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