/**********************************************
* Copyright (C) 2010 Lukas Laag
* This file is part of svgreal.
*
* svgreal is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* svgreal is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with svgreal. If not, see http://www.gnu.org/licenses/
**********************************************/
package org.vectomatic.svg.edit.client.engine;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.vectomatic.dom.svg.OMNode;
import org.vectomatic.dom.svg.OMSVGElement;
import org.vectomatic.dom.svg.impl.Attr;
import org.vectomatic.dom.svg.impl.NamedNodeMap;
import org.vectomatic.dom.svg.impl.SVGElement;
import org.vectomatic.dom.svg.itf.ISVGTransformable;
import org.vectomatic.dom.svg.utils.DOMHelper;
import org.vectomatic.dom.svg.utils.SVGConstants;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
/**
* Class to normalize id and idrefs in an SVG document
* @author laaglu
*/
public class SVGProcessor {
/*==========================================================
*
* E L E M E N T C L A S S I F I C A T I O N
*
*==========================================================*/
/**
* Tag names of definition elements which contain graphical
* elements but are not displayed directly
*/
protected static Set<String> definitionElementNames;
/**
* Tag names of graphical elements
*/
protected static Set<String> graphicalElementNames;
/**
* Tag names of group element
*/
protected static Set<String> groupElementNames;
public static boolean isGroupElement(SVGElement element) {
if (groupElementNames == null) {
groupElementNames = new HashSet<String>(Arrays.asList(new String[] {
SVGConstants.SVG_G_TAG,
SVGConstants.SVG_DEFS_TAG
}));
}
return groupElementNames.contains(DOMHelper.getLocalName(element));
}
/**
* Returns true if the specified node is a definition element.
* @param element the element to test.
* @return true if the specified node is a definition element.
*/
public static boolean isDefinitionElement(SVGElement element) {
if (definitionElementNames == null) {
definitionElementNames = new HashSet<String>(Arrays.asList(new String[] {
SVGConstants.SVG_SYMBOL_TAG,
SVGConstants.SVG_DEFS_TAG,
SVGConstants.SVG_PATTERN_TAG,
SVGConstants.SVG_MARKER_TAG,
SVGConstants.SVG_CLIP_PATH_TAG,
SVGConstants.SVG_MASK_TAG,
SVGConstants.SVG_GLYPH_TAG,
SVGConstants.SVG_MISSING_GLYPH_TAG
}));
}
return definitionElementNames.contains(DOMHelper.getLocalName(element));
}
/**
* Returns true if the specified node is a graphical element.
* @param element the element to test.
* @return true if the specified node is a graphical element.
*/
public static boolean isGraphicalElement(SVGElement element) {
if (graphicalElementNames == null) {
graphicalElementNames = new HashSet<String>(Arrays.asList(new String[] {
SVGConstants.SVG_CIRCLE_TAG,
SVGConstants.SVG_ELLIPSE_TAG,
SVGConstants.SVG_G_TAG,
SVGConstants.SVG_IMAGE_TAG,
SVGConstants.SVG_LINE_TAG,
SVGConstants.SVG_PATH_TAG,
SVGConstants.SVG_POLYLINE_TAG,
SVGConstants.SVG_POLYGON_TAG,
SVGConstants.SVG_RECT_TAG,
SVGConstants.SVG_TEXT_TAG,
SVGConstants.SVG_T_REF_TAG,
SVGConstants.SVG_T_SPAN_TAG,
SVGConstants.SVG_USE_TAG
}));
}
return graphicalElementNames.contains(DOMHelper.getLocalName(element));
}
/**
* Returns true if the specified node is an SVG element.
* @param node the node to test.
* @return true if the specified node is an SVG element.
*/
public static boolean isSvgElement(Node node) {
return node.getNodeType() == Node.ELEMENT_NODE
&& SVGConstants.SVG_NAMESPACE_URI.equals(DOMHelper.getNamespaceURI(node));
}
/**
* Returns true if the specified element is an SVG title or desc element.
* @param element the element to test.
* @return true if the specified element is an SVG title or desc element.
*/
public static boolean isTitleDescElement(SVGElement element) {
String localName = DOMHelper.getLocalName(element);
return SVGConstants.SVG_TITLE_TAG.equals(localName) || SVGConstants.SVG_DESC_TAG.equals(localName);
}
/**
* Returns true if the specified element implements ISVGTransformable.
* @param node the node to test.
* @return true if the specified element implements ISVGTransformable.
*/
public static boolean isTransformable(Node node) {
return OMNode.convert(node) instanceof ISVGTransformable;
}
/*==========================================================
*
* I D R E F E R E N C E S M A N A G E M E N T
*
*==========================================================*/
static Set<String> IDREF_ATTS = new HashSet<String>(Arrays.asList(
new String[] { "clip-path",
"mask",
"marker-start",
"marker-mid",
"marker-end",
"fill",
"stroke",
"filter",
"cursor",
"style"}));
static IdRefTokenizer TOKENIZER = GWT.create(IdRefTokenizer.class);
/**
* Adds the ids of all elements referred to by the specified element
* to the specified collection of referenced ids.
* @param element
* The element to analyze
* @param refs
* A collection of referenced ids.
*/
public static void getIdReferences(Collection<String> refs, Element element) {
NamedNodeMap<Attr> attrs = DOMHelper.getAttributes(element);
for (int i = 0, length = attrs.getLength(); i < length; i++) {
Attr attr = attrs.item(i);
if (IDREF_ATTS.contains(attr.getName())) {
TOKENIZER.tokenize(attr.getValue());
IdRefTokenizer.IdRefToken token;
while ((token = TOKENIZER.nextToken()) != null) {
if (token.getKind() == IdRefTokenizer.IdRefToken.IDREF) {
refs.add(token.getValue());
}
}
}
}
}
/*==========================================================
*
* M U L T I D O C U M E N T M A N A G E M E N T
*
*==========================================================*/
static int docId;
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
IdRefTokenizer tokenizer = new IdRefTokenizer();
StringBuilder builder = new StringBuilder();
tokenizer.tokenize(args[i]);
IdRefTokenizer.IdRefToken token;
while ((token = tokenizer.nextToken()) != null) {
String txt = (token.getKind() == IdRefTokenizer.IdRefToken.DATA) ? token.getValue() : ("{" + token.getValue() + "}");
builder.append(txt);
}
System.out.println("\"" + args[i] + "\" ==> \"" + builder.toString() + "\"");
}
}
/**
* Creates a new unique id prefix for id attributes of an svg model.
* The ids have the following structure:
* <pre>{prefix/ext1/...extN}localId</pre>
* where:
* <dl>
* <dt>prefix</dt><dd>is the unique document prefix returned by this method</dd>
* <dt>extK</dt><dd>is a model specific extension used to avoid collisions between virtual hiearchies created in the model (such as element/twin)</dd>
* <dt>localId</dt><dd>is the actual id appearing in the source document</dd>
* </dl>
* @return
*/
public static String newIdPrefix() {
docId++;
return "d" + docId;
}
/**
* Creates a new unique id prefix with the specified extension
* @param base The base prefix
* @param extension The extension to add
* @return The prefix id
*/
public static String newPrefixExtension(String base, String extension) {
return base + "/" + extension;
}
public static String makeId(String idPrefix, String localId) {
return "{" + idPrefix + "}" + localId;
}
/**
* Transforms all ids and id-refs in the specified source svg to avoid
* collisions with other svgs in other models.
* @param srcSvg The source svg
* @param idPrefix A prefix to apply to avoid collisions
*/
public static void normalizeIds(OMSVGElement srcSvg, String idPrefix) {
// Collect all the original element ids and replace them with a
// normalized id
int idIndex = 0;
Map<String, String> idToNormalizedId = new HashMap<String, String>();
List<Element> queue = new ArrayList<Element>();
queue.add(srcSvg.getElement());
while (queue.size() > 0) {
Element element = queue.remove(0);
String id = element.getId();
if (id != null) {
String normalizedId = makeId(idPrefix, Integer.toString(idIndex++));
idToNormalizedId.put(id, normalizedId);
element.setId(normalizedId);
}
NodeList<Node> childNodes = element.getChildNodes();
for (int i = 0, length = childNodes.getLength(); i < length; i++) {
Node childNode = childNodes.getItem(i);
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
queue.add((Element)childNode.cast());
}
}
}
// Change all the attributes which are URI references
queue.add(srcSvg.getElement());
while (queue.size() > 0) {
Element element = queue.remove(0);
if (DOMHelper.hasAttributeNS(element, SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE)) {
String idRef = DOMHelper.getAttributeNS(element, SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE);
// TODO: Test will probably not work on Opera (where all URLs are made absolute)
if (idRef.startsWith("#")) {
// Normalize hrefs to internal elements (such as between two gradients),
// not hrefs to external elements (such as between an image and its png)
String normalizeIdRef = idToNormalizedId.get(idRef.substring(1));
DOMHelper.setAttributeNS(element, SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, "#" + normalizeIdRef);
}
}
NamedNodeMap<Attr> attrs = DOMHelper.getAttributes(element);
for (int i = 0, length = attrs.getLength(); i < length; i++) {
Attr attr = attrs.item(i);
if (IDREF_ATTS.contains(attr.getName())) {
StringBuilder builder = new StringBuilder();
TOKENIZER.tokenize(attr.getValue());
IdRefTokenizer.IdRefToken token;
while ((token = TOKENIZER.nextToken()) != null) {
String value = token.getValue();
if (token.getKind() == IdRefTokenizer.IdRefToken.DATA) {
builder.append(value);
} else {
value = idToNormalizedId.get(value);
builder.append(value == null ? token.getValue() : value);
}
}
attr.setValue(builder.toString());
}
}
NodeList<Node> childNodes = element.getChildNodes();
for (int i = 0, length = childNodes.getLength(); i < length; i++) {
Node childNode = childNodes.getItem(i);
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
queue.add((Element)childNode.cast());
}
}
}
}
/**
* Transfers all the children for one element to another element
* @param src the source element
* @param dest the destination element
*/
public static void reparent(OMSVGElement src, OMSVGElement dest) {
Element srcElement = src.getElement();
Element destElement = dest.getElement();
Node node;
while((node = srcElement.getFirstChild()) != null) {
destElement.appendChild(srcElement.removeChild(node));
}
}
}