/******************************************************************************* * 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.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.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; 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.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.VRamBufferedImage; import org.simantics.scl.runtime.function.Function1; import org.simantics.scl.runtime.function.Function2; import org.simantics.utils.threads.AWTThread; 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; 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(); transient BufferedImage buffer = null; transient String documentCache = null; transient SVGDiagram diagramCache = null; transient String dataHash = null; static transient Map> bufferCache = new HashMap>(); @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; 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; } @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 if (!data.equals(documentCache) || diagramCache == null || buffer == null) initBuffer(g2d); AffineTransform ot = null; if (!transform.isIdentity()) { ot = g2d.getTransform(); g2d.transform(transform); } if (buffer != null) buffer.paint(g2d); if (ot != null) g2d.setTransform(ot); } 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); 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 boolean applyAssignments(SVGDiagram diagram, List assignments) throws SVGException { if (assignments.isEmpty()) return false; boolean changed = false; for (SVGNodeAssignment ass : assignments) { // System.err.println("assign: " + ass.elementId + " " + ass.attributeNameOrId + " " + ass.value); // if("opacity".equals(ass.attributeNameOrId)) // System.err.println("faaf"); SVGElement e = diagram.getElement(ass.elementId); if (e != null) { if ("$text".equals(ass.attributeNameOrId)) { if (e instanceof Tspan) { Tspan t = (Tspan) e; if (ass.value.trim().isEmpty()) { t.setText("-"); } else { t.setText(ass.value); } SVGElement parent = t.getParent(); if (parent instanceof Text) ((Text) parent).rebuild(); changed = true; } } else { e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value); changed = true; } } } diagram.updateTime(0); return changed; } public static Rectangle2D getBounds(String data) { return getBounds(data, null); } public static Rectangle2D getBounds(String data, List assignments) { if (data == null) { new Exception("null SVG data").printStackTrace(); return null; } SVGDiagram diagramCache = null; try { // NOTE: hard-coded to assume all SVG data is encoded in UTF-8 byte[] dataBytes = data.getBytes("UTF-8"); String digest = digest(dataBytes, assignments); SVGUniverse univ = SVGCache.getSVGUniverse(); // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have. synchronized (univ) { //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest); URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest); diagramCache = univ.getDiagram(uri, false); if (diagramCache != null) { if (diagramCache.getRoot() == null) { diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA)); } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) { diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA)); } } } Rectangle2D rect = null; if (diagramCache != null) { SVGRoot root = diagramCache.getRoot(); Rectangle2D bbox = root.getBoundingBox(); rect = (Rectangle2D) bbox.clone(); } else { rect = new Rectangle2D.Double(); } return rect; } catch (SVGException e) { return diagramCache.getViewRect(new Rectangle2D.Double()); } catch(IOException e) { } return null; } public static Rectangle2D getRealBounds(String data) { return getRealBounds(data, null); } public static Rectangle2D getRealBounds(String data, List assignments) { if (data == null) { new Exception("null SVG data").printStackTrace(); return null; } SVGDiagram diagramCache = null; try { // NOTE: hard-coded to assume all SVG data is encoded in UTF-8 byte[] dataBytes = data.getBytes("UTF-8"); String digest = digest(dataBytes, assignments); SVGUniverse univ = SVGCache.getSVGUniverse(); // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have. synchronized (univ) { //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest); URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest); diagramCache = univ.getDiagram(uri, false); if (diagramCache != null) { SVGRoot root = diagramCache.getRoot(); if (root == null) return new Rectangle2D.Double(); return (Rectangle2D)root.getBoundingBox().clone(); } } } catch (SVGException e) { return diagramCache.getViewRect(new Rectangle2D.Double()); } catch(IOException e) { } return null; } protected void initBuffer(Graphics2D g2d) { if (!data.equals(documentCache) || diagramCache == null) { dataHash = parseSVG(); if (diagramCache == null) { System.err.println("UNABLE TO PARSE SVG:\n" + data); return; } } if (buffer != null) { buffer = null; } diagramCache.setIgnoringClipHeuristic(true); // FIXME if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) { buffer = bufferCache.get(dataHash).get(); } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) { buffer = null; } else if(useMipMap) { if(G2DUtils.isAccelerated(g2d)) { buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize); } else { buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize); } bufferCache.put(dataHash, new WeakReference(buffer)); } else { if(G2DUtils.isAccelerated(g2d)) { buffer = new VRamBufferedImage(diagramCache, bounds, targetSize); } else { buffer = new BufferedImage(diagramCache, bounds, targetSize); } bufferCache.put(dataHash, new WeakReference(buffer)); } } public void setProperty(String field, Object value) { if("data".equals(field)) { // System.out.println("SVGNode data -> " + value); this.data = (String)value; } else if ("z".equals(field)) { // System.out.println("SVGNode z -> " + value); setZIndex((Integer)value); } else if ("position".equals(field)) { // System.out.println("SVGNode position -> " + value); Point point = (Point)value; setTransform(AffineTransform.getTranslateInstance(point.x, point.y)); // setPosition(point.x, point.y); } } @Override public void initValues() { data = defaultData; dataHash = null; buffer = null; } static WeakHashMap digestCache = new WeakHashMap(); static String digest(byte[] dataBytes, List assignments) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] messageDigest = md.digest(dataBytes); BigInteger number = new BigInteger(1, messageDigest); String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0); String result = digestCache.get(dataHash); if(result == null) { result = dataHash; digestCache.put(dataHash,dataHash); } return result; } catch (NoSuchAlgorithmException e) { // Shouldn't happen throw new Error("MD5 digest must exist."); } } static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg"); static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg"); @Override public Function1 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; } }