blob: 56aecefc61e6bcf511216c7a4746d604f481d04c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 BSI Business Systems Integration AG.
* 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:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.svg.client;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.dom.svg.SVGOMRect;
import org.apache.batik.swing.svg.GVTTreeBuilder;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.XMLConstants;
import org.eclipse.scout.commons.StringUtility;
import org.eclipse.scout.commons.exception.ProcessingException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.w3c.dom.svg.SVGAElement;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGPoint;
import org.w3c.dom.svg.SVGStylable;
import org.w3c.dom.svg.SVGTSpanElement;
import org.w3c.dom.svg.SVGTextContentElement;
import org.w3c.dom.svg.SVGTransform;
import org.w3c.dom.svg.SVGTransformList;
import org.w3c.dom.svg.SVGTransformable;
public final class SVGUtility {
public static final String SVG_NS = SVGDOMImplementation.SVG_NAMESPACE_URI;
public static final String XLINK_NS = XMLConstants.XLINK_NAMESPACE_URI;
/**
* Conversion of points/mm/inch and more see http://www.endmemo.com/convert/topography.php
*/
private static final float PIXEL_PER_POINT = 1.333333f;
private static final float PIXEL_PER_MM = 3.779528f;
private static final float PIXEL_PER_INCH = 96f;
private static final float DEFAULT_FONT_HEIGHT = 14f;
public interface INodeVisitor {
/**
* @return true to continue visiting, false to stop
*/
boolean visit(Node node) throws Exception;
}
private SVGUtility() {
}
/**
* Parses a SVG document read by the given input stream. The document returned can be modified on XML-level. If you
* need to perform any CSS, text size and and bounding box operations use
* {@link #readSVGDocumentForGraphicalModification(InputStream)} instead.
*
* @param in
* input stream the SVG document is read from.
* @return Returns the SVG document.
*/
public static SVGDocument readSVGDocument(InputStream in) throws ProcessingException {
String cn;
try {
cn = Class.forName("org.apache.xerces.parsers.SAXParser").getName();
}
catch (Throwable t) {
try {
cn = Class.forName("com.sun.org.apache.xerces.internal.parsers.SAXParser").getName();
}
catch (Exception e) {
throw new ProcessingException("Finding SAXParser", e);
}
}
SAXSVGDocumentFactory documentFactory = new SAXSVGDocumentFactory(cn);
documentFactory.setValidating(false);
SVGDocument doc;
try {
doc = documentFactory.createSVGDocument(null, in);
}
catch (Exception e) {
throw new ProcessingException("Reading SVG Failed", e);
}
try {
doc.setDocumentURI("urn:svg");//needed to make anchors work but only works in dom level 3
}
catch (Throwable t) {
//nop, dom level less than 3
}
return doc;
}
/**
* Parses a SVG document read by the given input stream and attaches a GVT tree. An attached GVT tree is required for
* performing CSS, text size and and bounding box operations on the SVG document.
* <p/>
* The resulting bridge context holds a reference to the SVG document {@link BridgeContext#getDocument()}
* <p/>
* <h1>Important:</h1> Callers are required to invoke {@link BridgeContext#dispose()} on the returned bridge context
* as soon as the bridge or the document it references is not required anymore.
* <p/>
* If the documents needs not be manipulated, use {@link #readSVGDocument(InputStream)} instead.
*
* @param in
* input stream the SVG document is read from.
* @return Returns a bridge context that holds references to the SVG document as well as to the GVT tree wrapping
* objects.
*/
public static BridgeContext readSVGDocumentForGraphicalModification(InputStream in) throws ProcessingException {
SVGDocument doc = readSVGDocument(in);
//add a gvt tree for text and alignment calculations
BridgeContext bc = new BridgeContext(new UserAgentAdapter());
bc.setDynamic(true);
GVTTreeBuilder treeBuilder = new GVTTreeBuilder(doc, bc);
treeBuilder.setPriority(Thread.MAX_PRIORITY);
treeBuilder.run();
return bc;
}
public static void writeSVGDocument(SVGDocument doc, OutputStream out, String encoding) throws ProcessingException {
try {
DOMSource domSource = new DOMSource(doc);
StreamResult streamResult = new StreamResult(out);
Transformer t = TransformerFactory.newInstance().newTransformer();
if (encoding != null) {
t.setOutputProperty("encoding", encoding);
}
t.transform(domSource, streamResult);
out.close();
}
catch (Exception e) {
throw new ProcessingException("Writing SVG Failed", e);
}
}
public static List<Element> getElementsAt(SVGDocument doc, SVGPoint point) {
ArrayList<Element> list = new ArrayList<Element>();
SVGOMRect svgOMRect = new SVGOMRect(point.getX(), point.getY(), 1, 1);
NodeList intersectedElements = doc.getRootElement().getIntersectionList(svgOMRect, null);
int n = intersectedElements.getLength();
for (int i = 0; i < n; i++) {
Node node = intersectedElements.item(i);
if (node instanceof Element) {
list.add((Element) node);
}
}
return list;
}
/**
* @return true if whole tree was visited, false if a {@link INodeVisitor#visit(Node)} returned false
*/
public static boolean visitDocument(Document doc, INodeVisitor v) throws Exception {
return visitNode(doc.getDocumentElement(), v);
}
/**
* @return true if whole sub-tree was visited, false if a {@link INodeVisitor#visit(Node)} returned false
*/
public static boolean visitNode(Node parent, INodeVisitor v) throws Exception {
NodeList nl = parent.getChildNodes();
boolean b;
for (int i = 0; i < nl.getLength(); i++) {
b = v.visit(nl.item(i));
if (!b) {
return false;
}
b = visitNode(nl.item(i), v);
if (!b) {
return false;
}
}
return true;
}
public static void setTransform(SVGElement e, float x, float y, float rotation) {
SVGTransformList list = ((SVGTransformable) e).getTransform().getBaseVal();
list.clear();
if (rotation != 0) {
SVGTransform tx = e.getOwnerSVGElement().createSVGTransform();
tx.setRotate(rotation, 0, 0);
list.appendItem(tx);
}
if (x != 0 || y != 0) {
SVGTransform tx = e.getOwnerSVGElement().createSVGTransform();
tx.setTranslate(x, y);
list.appendItem(tx);
}
}
/**
* Set the text content of a text element, in case it contains newlines then add tspan elements.
* Requires the GVT tree to be attached to the svg document.
*
* @param textElement
* @param value
* @param rowGap
* in px
*/
public static void setTextContent(Element e, String value, final Float rowGap) {
if (e == null) {
return;
}
SVGTextContentElement textElement = (SVGTextContentElement) e;
//remove inner tspan elements
NodeList nl = textElement.getElementsByTagName(SVGConstants.SVG_TSPAN_TAG);
for (int i = 0; i < nl.getLength(); i++) {
nl.item(i).getParentNode().removeChild(nl.item(i));
}
if (value == null || value.length() == 0) {
setTextContent(textElement, null);
return;
}
if (!value.contains("\n")) {
setTextContent(textElement, value);
return;
}
//get font height
float fontHeight = 0f;
Node tmpNode = textElement;
while (fontHeight == 0f && tmpNode != null) {
if (tmpNode instanceof SVGStylable) {
//get font height
String fontSizeText = ((SVGStylable) tmpNode).getStyle().getPropertyValue(SVGConstants.CSS_FONT_SIZE_PROPERTY);
if (fontSizeText != null) {
fontHeight = convertToPx(fontSizeText);
break;
}
}
//next
tmpNode = tmpNode.getParentNode();
}
if (fontHeight == 0f) {
fontHeight = DEFAULT_FONT_HEIGHT;
}
Float rGap = rowGap == null ? Float.valueOf(1f) : rowGap;
float rowHeight = fontHeight + rGap;
//create tspan lines
float y = 0;
setTextContent(textElement, null);
for (String line : value.split("[\n\r]")) {
SVGTSpanElement tspanElem = (SVGTSpanElement) textElement.getOwnerDocument().createElementNS(SVG_NS, SVGConstants.SVG_TSPAN_TAG);
textElement.appendChild(tspanElem);
tspanElem.setTextContent(line);
tspanElem.setAttribute("x", "0");
tspanElem.setAttribute("y", String.valueOf(y));
y += rowHeight;
}
}
/**
* @param set
* the text content on a node by using the child text node. Use this instead of e.setTextContent to be
* compatible with batik 1.6 (jdk 1.4)
*/
public static void setTextContent(Element e, String textContent) {
//remove children
while (e.getFirstChild() != null) {
e.removeChild(e.getFirstChild());
}
//add child text node
Text textNode = e.getOwnerDocument().createTextNode(textContent);
e.appendChild(textNode);
}
/**
* This feature is experimental as a convenience since svg does not support native text operations.
*
* @param contextElement
* is the {@link SVGTextContentElement} containing optional style and font information context for the
* wrapping algorithm
* @param text
* @param wordWrapWidth
* in px
* @return the wrapped text with additional newline characters where it was wrapped.
*/
public static String wrapText(SVGTextContentElement contextElement, String text, Float wordWrap) {
if (text == null) {
return "";
}
List<String> lines = Arrays.asList(text.split("[\n\r]"));
if (wordWrap == null || wordWrap <= 0 || text.length() == 0) {
return text;
}
float wrap = wordWrap.floatValue();
ArrayList<String> wrappedLines = new ArrayList<String>(lines.size());
for (String line : lines) {
if (!StringUtility.hasText(line)) {
wrappedLines.add("");
continue;
}
line = line.replaceAll("[\\s]+", " ").trim();
try {
setTextContent(contextElement, line);
float[] w = new float[line.length()];
for (int i = 0; i < w.length; i++) {
w[i] = contextElement.getExtentOfChar(i).getWidth();
}
//
String[] words = line.split("[ ]");
int startIndex = 0;
int endIndex = 0;
float acc = 0;
StringBuilder lineBuf = new StringBuilder();
for (int wordIndex = 0; wordIndex < words.length; wordIndex++) {
String word = words[wordIndex];
endIndex = startIndex + word.length();
float dw = 0;
for (int i = startIndex; i < endIndex; i++) {
dw += w[i];
}
//wrap when there is at least one word and text exceeds line
if (lineBuf.length() > 0 && acc + dw > wrap) {
//maybe text is absolutely too large
if (acc > wrap) {
wrappedLines.add(rtrim(clipText(contextElement, lineBuf.toString(), wrap)));
}
else {
wrappedLines.add(rtrim(lineBuf.toString()));
}
lineBuf.setLength(0);
acc = 0;
}
acc += dw;
lineBuf.append(word);
//also add following space
if (endIndex < w.length) {
acc += w[endIndex];
lineBuf.append(" ");
}
//next (+1: skip following space)
startIndex = endIndex + 1;
}
//remaining text
if (lineBuf.length() > 0) {
//maybe text is absolutely too large
if (acc > wrap) {
wrappedLines.add(rtrim(clipText(contextElement, lineBuf.toString(), wrap)));
}
else {
wrappedLines.add(rtrim(lineBuf.toString()));
}
lineBuf.setLength(0);
}
}
finally {
setTextContent(contextElement, null);
}
}
while (wrappedLines.size() > 0 && wrappedLines.get(wrappedLines.size() - 1).length() == 0) {
wrappedLines.remove(wrappedLines.size() - 1);
}
StringBuilder buf = new StringBuilder();
for (int i = 0, n = wrappedLines.size(); i < n; i++) {
if (i > 0) {
buf.append("\n");
}
buf.append(wrappedLines.get(i));
}
return buf.toString();
}
private static String rtrim(String s) {
int len = s.length();
int r = len - 1;
while (r >= 0 && s.charAt(r) <= ' ') {
r--;
}
if (r == len - 1) {
return s;
}
if (r < 0) {
return "";
}
return s.substring(0, r + 1);
}
/**
* This feature is experimental as a convenience since svg does not support native text operations.
*
* @param contextElement
* is the {@link SVGTextContentElement} containing optional style and font information context for the
* wrapping algorithm
* @param text
* is one line of text (no newlines)
* @param clipWidth
* in px
* @return the text clipped to fit the clipWidth. If the text is too large it is cropped and "..." is appended at the
* end.
*/
public static String clipText(SVGTextContentElement contextElement, String text, float clipWidth) {
if (text == null || text.length() == 0) {
return text;
}
if (clipWidth <= 0) {
return text;
}
String suffix = "...";
try {
setTextContent(contextElement, text + suffix);
int textLen = text.length();
int suffixLen = suffix.length();
float textWidth = 0;
float suffixWidth = 0;
float[] w = new float[textLen + suffixLen];
for (int i = 0; i < w.length; i++) {
w[i] = contextElement.getExtentOfChar(i).getWidth();
if (i < textLen) {
textWidth += w[i];
}
else {
suffixWidth += w[i];
}
}
if (textWidth <= clipWidth) {
return text;
}
int i = textLen - 1;
while (i > 0 && textWidth + suffixWidth > clipWidth) {
textWidth -= w[i];
i--;
}
return text.substring(0, i + 1) + suffix;
}
finally {
setTextContent(contextElement, null);
}
}
/**
* This feature is experimental as a convenience since svg does not support native text operations.
*
* @param contextElement
* is the {@link SVGTextContentElement} containing optional style and font information context for the
* wrapping algorithm
* @param text
* is one line of text (no newlines)
* @return the text width in pixels
*/
public static float getTextWidth(SVGTextContentElement contextElement, String text) {
if (text == null || text.length() == 0) {
return 0;
}
try {
setTextContent(contextElement, text);
int textLen = text.length();
float textWidth = 0;
for (int i = 0; i < textLen; i++) {
textWidth += contextElement.getExtentOfChar(i).getWidth();
}
return textWidth;
}
finally {
setTextContent(contextElement, null);
}
}
private static float convertToPx(String valueWithUnit) {
Matcher m = Pattern.compile("([0-9.]+)([^0-9.]*)").matcher(valueWithUnit);
m.matches();
float f = Float.valueOf(m.group(1));
String unit = m.group(2);
if (unit == null) {
return f;
}
unit = unit.toLowerCase();
if ("px".equals(unit)) {
return f;
}
if ("pt".equals(unit)) {
return f * PIXEL_PER_POINT;
}
if ("mm".equals(unit)) {
return f * PIXEL_PER_MM;
}
if ("in".equals(unit)) {
return f * PIXEL_PER_INCH;
}
return f;
}
/**
* Enclose the element with a link to an url
* <p>
* Bug fix: batik sometimes creates a new namespace for xlink even though the namespace is already defined. This
* utility method fixes that behaviour.
* <p>
* Bug: <xmp><a xlink:actuate="onRequest" xlink:type="simple" xlink:show="replace"
* xmlns:ns3="http://www.w3.org/1999/xlink" ns3:href="http://local/info-prev">....</a></xmp>
* <p>
* Fixed: <xmp><a xlink:actuate="onRequest" xlink:type="simple" xlink:show="replace"
* xlink:href="http://local/info-prev">....</a></xmp>
*/
public static void addHyperlink(Element e, String url) {
SVGAElement aElem = (SVGAElement) e.getOwnerDocument().createElementNS(SVG_NS, "a");
e.getParentNode().insertBefore(aElem, e);
e.getParentNode().removeChild(e);
aElem.appendChild(e);
aElem.getHref().setBaseVal(url);
//bug fix: remove xmlns:xlink=... attributes, change attributes of ns xlink to have name prefixed with 'xlink'
NamedNodeMap nnmap = aElem.getAttributes();
for (int i = 0, n = nnmap.getLength(); i < n; i++) {
Node node = nnmap.item(i);
if (node instanceof Attr) {
Attr a = (Attr) node;
if (XLINK_NS.equals(a.getNamespaceURI()) && !"xlink".equals(a.getPrefix())) {
nnmap.removeNamedItemNS(a.getNamespaceURI(), a.getLocalName());
a.setPrefix("xlink");
nnmap.setNamedItemNS(a);
}
}
}
for (int i = 0, n = nnmap.getLength(); i < n; i++) {
Node node = nnmap.item(i);
if (node instanceof Attr) {
Attr a = (Attr) node;
if (javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(a.getNamespaceURI())) {
nnmap.removeNamedItemNS(a.getNamespaceURI(), a.getLocalName());
}
}
}
}
}