/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.scenegraph.g2d.nodes; import java.awt.Graphics2D; import java.awt.Point; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.lang.ref.WeakReference; import java.math.BigInteger; import java.net.URI; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget; import org.simantics.scenegraph.LoaderNode; import org.simantics.scenegraph.ScenegraphUtils; import org.simantics.scenegraph.g2d.G2DNode; import org.simantics.scenegraph.g2d.G2DRenderingHints; import org.simantics.scenegraph.utils.BufferedImage; import org.simantics.scenegraph.utils.G2DUtils; import org.simantics.scenegraph.utils.InitValueSupport; import org.simantics.scenegraph.utils.MipMapBufferedImage; import org.simantics.scenegraph.utils.MipMapVRamBufferedImage; import org.simantics.scenegraph.utils.SVGPassthruShape; import org.simantics.scenegraph.utils.VRamBufferedImage; import org.simantics.scl.runtime.function.Function1; import org.simantics.scl.runtime.function.Function2; import org.simantics.utils.threads.AWTThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import com.kitfox.svg.RenderableElement; import com.kitfox.svg.SVGCache; import com.kitfox.svg.SVGDiagram; import com.kitfox.svg.SVGElement; import com.kitfox.svg.SVGException; import com.kitfox.svg.SVGRoot; import com.kitfox.svg.SVGUniverse; import com.kitfox.svg.Text; import com.kitfox.svg.Tspan; import com.kitfox.svg.animation.AnimationElement; @RasterOutputWidget public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode { private static final long serialVersionUID = 8508750881358776559L; private static final Logger LOGGER = LoggerFactory.getLogger(SVGNode.class); protected String data = null; protected String defaultData = null; protected Point targetSize = null; protected Boolean useMipMap = true; protected Rectangle2D bounds = null; protected List assignments = new ArrayList(); protected transient BufferedImage buffer = null; protected transient String documentCache = null; protected transient SVGDiagram diagramCache = null; protected transient String dataHash = null; static transient Map> bufferCache = new HashMap>(); @Override public void init() { super.init(); } @Override public void cleanup() { cleanDiagramCache(); } public void setAssignments(List ass) { assignments.clear(); assignments.addAll(ass); } public void cleanDiagramCache() { SVGDiagram d = diagramCache; if (d != null) { diagramCache = null; dataHash = null; SVGUniverse univ = SVGCache.getSVGUniverse(); if (univ.decRefCountAndClear(d.getXMLBase()) == 0) { // Cleared! //System.out.println("cleared: " + d.getXMLBase()); } } } static WeakHashMap dataCache = new WeakHashMap(); @PropertySetter("SVG") @SyncField("data") public void setData(String data) { String cached = dataCache.get(data); if (cached == null) { cached = data; dataCache.put(data, data); } this.data = cached; this.defaultData = cached; cleanDiagramCache(); } @SyncField("targetSize") public void setTargetSize(Point p) { this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason } @SyncField("targetSize") public void setTargetSize(int x, int y) { this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason } @SyncField("useMipMap") public void useMipMap(Boolean use) { this.useMipMap = use; } @PropertySetter("Bounds") @SyncField("bounds") public void setBounds(Rectangle2D bounds) { this.bounds = bounds; } @Override public Rectangle2D getBoundsInLocal() { if (bounds == null) parseSVG(); return bounds; } @Override public void render(Graphics2D g2d) { if (data == null) return; // Not initialized AffineTransform ot = null; if (!transform.isIdentity()) { ot = g2d.getTransform(); g2d.transform(transform); } if (g2d.getRenderingHint(G2DRenderingHints.KEY_SVG_PASSTHRU) == Boolean.TRUE) { SVGPassthruShape.resetG2D(g2d); String svg = assignments.isEmpty() ? data : applyAssigments(data, assignments); if (svg != null) { g2d.fill(new SVGPassthruShape(svg)); } } else { if (!data.equals(documentCache) || diagramCache == null || buffer == null) initBuffer(g2d); if (buffer != null) buffer.paint(g2d); } if (ot != null) g2d.setTransform(ot); } protected int dynamicHash() { return 0; } protected String parseSVG() { if (data == null) return null; SVGUniverse univ = SVGCache.getSVGUniverse(); try { Rectangle2D bbox = null; synchronized (univ) { // Relinquish reference to current element if (diagramCache != null) { univ.decRefCount(diagramCache.getXMLBase()); diagramCache = null; } // Lets check for rootAssignment that contributes the whole SVG SVGNodeAssignment rootAssignment = null; if (!assignments.isEmpty()) { for (SVGNodeAssignment ass : assignments) { if (ass.attributeNameOrId.equals("$root")) { rootAssignment = ass; break; } } } byte[] dataBytes; if (rootAssignment != null) { dataBytes = rootAssignment.value.getBytes("UTF-8"); } else { // NOTE: hard-coded to assume all SVG data is encoded in UTF-8 dataBytes = data.getBytes("UTF-8"); } dataHash = digest(dataBytes, assignments, dynamicHash()); URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash); diagramCache = univ.getDiagram(uri, false); if (diagramCache != null) { univ.incRefCount(diagramCache.getXMLBase()); SVGRoot root = diagramCache.getRoot(); if (root == null) { univ.decRefCount(diagramCache.getXMLBase()); diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false); dataHash = "broken"; univ.incRefCount(diagramCache.getXMLBase()); bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone(); } else { bbox = root.getBoundingBox(); if (bbox.isEmpty()) { // Lets check if this should be visible or not Set presentationAttributes = root.getPresentationAttributes(); if (!presentationAttributes.contains("display")) { // TODO: fix this - How can one read values of attributes in SVG salamander??? univ.decRefCount(diagramCache.getXMLBase()); diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false); dataHash = "empty"; univ.incRefCount(diagramCache.getXMLBase()); bbox = (Rectangle2D) root.getBoundingBox().clone(); } else { bbox = new Rectangle2D.Double(0, 0, 0, 0); } } else { if (applyAssignments(diagramCache, assignments)) { bbox = (Rectangle2D) root.getBoundingBox().clone(); } else { bbox = (Rectangle2D) bbox.clone(); } } } } else { bbox = new Rectangle2D.Double(); } } documentCache = data; setBounds(bbox); } catch (SVGException e) { // This can only occur if diagramCache != null. setBounds(diagramCache.getViewRect(new Rectangle2D.Double())); univ.decRefCount(diagramCache.getXMLBase()); diagramCache = null; } catch (IOException e) { diagramCache = null; } return dataHash; } private static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); static { dbf.setValidating(false); try { dbf.setFeature("http://xml.org/sax/features/namespaces", false); dbf.setFeature("http://xml.org/sax/features/validation", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } catch (ParserConfigurationException e) { // Nothing to do } } // Notice: Remember to change both implementations of applyAssigments when modifying the functionality. protected static String applyAssigments(String svg, List assignments) { try { DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new InputSource(new StringReader(svg))); NodeList entries = doc.getElementsByTagName("*"); for (int i=0; i getPropertyFunction(String propertyName) { return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName); } @Override public T getProperty(String propertyName) { return null; } @Override public void setPropertyCallback(Function2 callback) { } public void synchronizeDocument(String document) { setData(document); } public void synchronizeTransform(double[] data) { this.setTransform(new AffineTransform(data)); } public String getSVGText() { String ret = data.replace("", "g>"); //return diagramCache.toString(); //return data.replace("", "/g>"); return ret; } public Rectangle2D getElementBounds(String id) throws SVGException { SVGElement e = diagramCache.getElement(id); if (e instanceof RenderableElement) { return ((RenderableElement)e).getBoundingBox(); } else { return null; } } }