/******************************************************************************* * 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.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 { public static class SVGNodeAssignment { public String elementId; public String attributeNameOrId; public String value; public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) { this.elementId = elementId; this.attributeNameOrId = attributeNameOrId; this.value = value; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode()); result = prime * result + ((elementId == null) ? 0 : elementId.hashCode()); result = prime * result + ((value == null) ? 0 : value.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SVGNodeAssignment other = (SVGNodeAssignment) obj; if (attributeNameOrId == null) { if (other.attributeNameOrId != null) return false; } else if (!attributeNameOrId.equals(other.attributeNameOrId)) return false; if (elementId == null) { if (other.elementId != null) return false; } else if (!elementId.equals(other.elementId)) return false; if (value == null) { if (other.value != null) return false; } else if (!value.equals(other.value)) return false; return true; } } 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; try { SVGUniverse univ = SVGCache.getSVGUniverse(); synchronized(univ) { // NOTE: hard-coded to assume all SVG data is encoded in UTF-8 byte[] dataBytes = data.getBytes("UTF-8"); dataHash = digest(dataBytes, assignments); if (diagramCache != null) univ.decRefCount(diagramCache.getXMLBase()); URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash); diagramCache = univ.getDiagram(uri, false); if (diagramCache != null) { if (diagramCache.getRoot() == null) { diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false); dataHash = "broken"; } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) { diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false); dataHash = "empty"; } else { for(SVGNodeAssignment ass : assignments) { SVGElement e = diagramCache.getElement(ass.elementId); if(e != null) { if("$text".equals(ass.attributeNameOrId)) { Tspan t = (Tspan)e; t.setText(ass.value); Text text = (Text)t.getParent(); text.rebuild(); } else { e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value); } } } if(!assignments.isEmpty()) diagramCache.updateTime(0); } } documentCache = data; if (diagramCache != null) { setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone()); univ.incRefCount(diagramCache.getXMLBase()); } else { setBounds(new Rectangle2D.Double()); } } } catch (SVGException e) { setBounds((Rectangle2D) diagramCache.getViewRect().clone()); } catch (IOException e) { diagramCache = null; } return dataHash; } 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 ((Rectangle2D) diagramCache.getViewRect().clone()); } 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 ((Rectangle2D) diagramCache.getViewRect().clone()); } 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)); } }