View Javadoc

1   /**********************************************
2    * Copyright (C) 2010 Lukas Laag
3    * This file is part of svgreal.
4    * 
5    * svgreal is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    * 
10   * svgreal is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   * 
15   * You should have received a copy of the GNU General Public License
16   * along with svgreal.  If not, see http://www.gnu.org/licenses/
17   **********************************************/
18  package org.vectomatic.svg.edit.client.engine;
19  
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import org.vectomatic.dom.svg.OMNode;
30  import org.vectomatic.dom.svg.OMSVGElement;
31  import org.vectomatic.dom.svg.impl.Attr;
32  import org.vectomatic.dom.svg.impl.NamedNodeMap;
33  import org.vectomatic.dom.svg.impl.SVGElement;
34  import org.vectomatic.dom.svg.itf.ISVGTransformable;
35  import org.vectomatic.dom.svg.utils.DOMHelper;
36  import org.vectomatic.dom.svg.utils.SVGConstants;
37  
38  import com.google.gwt.core.client.GWT;
39  import com.google.gwt.dom.client.Element;
40  import com.google.gwt.dom.client.Node;
41  import com.google.gwt.dom.client.NodeList;
42  
43  /**
44   * Class to normalize id and idrefs in an SVG document
45   * @author laaglu
46   */
47  public class SVGProcessor {
48  	/*==========================================================
49  	 * 
50  	 * E L E M E N T   C L A S S I F I C A T I O N
51  	 * 
52  	 *==========================================================*/
53  	/**
54  	 * Tag names of definition elements which contain graphical
55  	 * elements but are not displayed directly
56  	 */
57  	protected static Set<String> definitionElementNames;
58  	/**
59  	 * Tag names of graphical elements
60  	 */
61  	protected static Set<String> graphicalElementNames;
62  	/**
63  	 * Tag names of group element
64  	 */
65  	protected static Set<String> groupElementNames;
66  
67  	public static boolean isGroupElement(SVGElement element) {
68  		if (groupElementNames == null) {
69  			groupElementNames = new HashSet<String>(Arrays.asList(new String[] {
70  					SVGConstants.SVG_G_TAG,
71  					SVGConstants.SVG_DEFS_TAG
72  				}));
73  		}
74  		return groupElementNames.contains(DOMHelper.getLocalName(element));
75  	}
76  
77  	/**
78  	 * Returns true if the specified node is a definition element.
79  	 * @param element the element to test.
80  	 * @return true if the specified node is a definition element.
81  	 */
82  	public static boolean isDefinitionElement(SVGElement element) {
83  		if (definitionElementNames == null) {
84  			definitionElementNames = new HashSet<String>(Arrays.asList(new String[] {
85  					SVGConstants.SVG_SYMBOL_TAG,
86  					SVGConstants.SVG_DEFS_TAG,
87  					SVGConstants.SVG_PATTERN_TAG,
88  					SVGConstants.SVG_MARKER_TAG,
89  					SVGConstants.SVG_CLIP_PATH_TAG,
90  					SVGConstants.SVG_MASK_TAG,
91  					SVGConstants.SVG_GLYPH_TAG,
92  					SVGConstants.SVG_MISSING_GLYPH_TAG
93  				}));
94  		}
95  		return definitionElementNames.contains(DOMHelper.getLocalName(element));
96  	}
97  	
98  	/**
99  	 * Returns true if the specified node is a graphical element.
100 	 * @param element the element to test.
101 	 * @return true if the specified node is a graphical element.
102 	 */
103 	public static boolean isGraphicalElement(SVGElement element) {
104 		if (graphicalElementNames == null) {
105 			graphicalElementNames = new HashSet<String>(Arrays.asList(new String[] {
106 			SVGConstants.SVG_CIRCLE_TAG,
107 			SVGConstants.SVG_ELLIPSE_TAG,
108 			SVGConstants.SVG_G_TAG,
109 			SVGConstants.SVG_IMAGE_TAG,
110 			SVGConstants.SVG_LINE_TAG,
111 			SVGConstants.SVG_PATH_TAG,
112 			SVGConstants.SVG_POLYLINE_TAG,
113 			SVGConstants.SVG_POLYGON_TAG,
114 			SVGConstants.SVG_RECT_TAG,
115 			SVGConstants.SVG_TEXT_TAG,
116 			SVGConstants.SVG_T_REF_TAG,
117 			SVGConstants.SVG_T_SPAN_TAG,
118 			SVGConstants.SVG_USE_TAG
119 			}));	
120 		}
121 		return graphicalElementNames.contains(DOMHelper.getLocalName(element));
122 	}
123 	
124 	/**
125 	 * Returns true if the specified node is an SVG element.
126 	 * @param node the node to test.
127 	 * @return true if the specified node is an SVG element.
128 	 */
129 	public static boolean isSvgElement(Node node) {
130 		return node.getNodeType() == Node.ELEMENT_NODE 
131 			&& SVGConstants.SVG_NAMESPACE_URI.equals(DOMHelper.getNamespaceURI(node));
132 	}
133 	
134 	/**
135 	 * Returns true if the specified element is an SVG title or desc element.
136 	 * @param element the element to test.
137 	 * @return true if the specified element is an SVG title or desc element.
138 	 */
139 	public static boolean isTitleDescElement(SVGElement element) {
140 		String localName = DOMHelper.getLocalName(element);
141 		return SVGConstants.SVG_TITLE_TAG.equals(localName) || SVGConstants.SVG_DESC_TAG.equals(localName);
142 	}
143 
144 	/**
145 	 * Returns true if the specified element implements ISVGTransformable.
146 	 * @param node the node to test.
147 	 * @return true if the specified element implements ISVGTransformable.
148 	 */
149 	public static boolean isTransformable(Node node) {
150 		return OMNode.convert(node) instanceof ISVGTransformable;
151 	}
152 	
153 	/*==========================================================
154 	 * 
155 	 * I D   R E F E R E N C E S   M A N A G E M E N T 
156 	 * 
157 	 *==========================================================*/
158 	
159 	static Set<String> IDREF_ATTS = new HashSet<String>(Arrays.asList(
160 			new String[] { "clip-path",
161 			 "mask",
162 			 "marker-start",
163 			 "marker-mid",
164 			 "marker-end",
165 			 "fill",
166 			 "stroke",
167 			 "filter",
168 			 "cursor",
169 			 "style"}));
170 	static IdRefTokenizer TOKENIZER = GWT.create(IdRefTokenizer.class);
171 
172 	/**
173 	 * Adds the ids of all elements referred to by the specified element
174 	 * to the specified collection of referenced ids.
175 	 * @param element
176 	 * The element to analyze
177 	 * @param refs
178 	 * A collection of referenced ids.
179 	 */
180 	public static void getIdReferences(Collection<String> refs, Element element) {
181 		NamedNodeMap<Attr> attrs = DOMHelper.getAttributes(element);
182 		for (int i = 0, length = attrs.getLength(); i < length; i++) {
183 			Attr attr = attrs.item(i);
184 			if (IDREF_ATTS.contains(attr.getName())) {
185 				TOKENIZER.tokenize(attr.getValue());
186 				IdRefTokenizer.IdRefToken token;
187 				while ((token = TOKENIZER.nextToken()) != null) {
188 					if (token.getKind() == IdRefTokenizer.IdRefToken.IDREF) {
189 						refs.add(token.getValue());
190 					}
191 				}
192 			}
193 		}
194 	}
195 
196 
197 	/*==========================================================
198 	 * 
199 	 * M U L T I D O C U M E N T   M A N A G E M E N T 
200 	 * 
201 	 *==========================================================*/
202 	
203 	static int docId;
204 	public static void main(String[] args) {
205 		for (int i = 0; i < args.length; i++) {
206 			IdRefTokenizer tokenizer = new IdRefTokenizer();
207 			StringBuilder builder = new StringBuilder();
208 			tokenizer.tokenize(args[i]);
209 			IdRefTokenizer.IdRefToken token;
210 			while ((token = tokenizer.nextToken()) != null) {
211 				String txt = (token.getKind() == IdRefTokenizer.IdRefToken.DATA) ? token.getValue() : ("{" + token.getValue() + "}");
212 				builder.append(txt);
213 			}
214 			System.out.println("\"" + args[i] + "\" ==> \"" + builder.toString() + "\"");
215 		}
216 	}
217 	
218 	/**
219 	 * Creates a new unique id prefix for id attributes of an svg model.
220 	 * The ids have the following structure:
221 	 * <pre>{prefix/ext1/...extN}localId</pre>
222 	 * where:
223 	 * <dl>
224 	 * <dt>prefix</dt><dd>is the unique document prefix returned by this method</dd>
225 	 * <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>
226 	 * <dt>localId</dt><dd>is the actual id appearing in the source document</dd>
227 	 * </dl>
228 	 * @return
229 	 */
230 	public static String newIdPrefix() {
231 		docId++;
232 		return "d" + docId;
233 	}
234 	
235 	/**
236 	 * Creates a new unique id prefix with the specified extension
237 	 * @param base The base prefix
238 	 * @param extension The extension to add
239 	 * @return The prefix id
240 	 */
241 	public static String newPrefixExtension(String base, String extension) {
242 		return base + "/" + extension;
243 	}
244 	
245 	public static String makeId(String idPrefix, String localId) {
246 		return "{" + idPrefix + "}" + localId;
247 	}
248 	
249 	/**
250 	 * Transforms all ids and id-refs in the specified source svg to avoid
251 	 * collisions with other svgs in other models.
252 	 * @param srcSvg The source svg
253 	 * @param idPrefix A prefix to apply to avoid collisions
254 	 */
255 	public static void normalizeIds(OMSVGElement srcSvg, String idPrefix) {
256 		// Collect all the original element ids and replace them with a
257 		// normalized id
258 		int idIndex = 0;
259 		Map<String, String> idToNormalizedId = new HashMap<String, String>();
260 		List<Element> queue = new ArrayList<Element>();
261 		queue.add(srcSvg.getElement());
262 		while (queue.size() > 0) {
263 			Element element = queue.remove(0);
264 			String id = element.getId();
265 			if (id != null) {
266 				String normalizedId = makeId(idPrefix, Integer.toString(idIndex++));
267 				idToNormalizedId.put(id, normalizedId);
268 				element.setId(normalizedId);
269 			}
270 			NodeList<Node> childNodes =  element.getChildNodes();
271 			for (int i = 0, length = childNodes.getLength(); i < length; i++) {
272 				Node childNode = childNodes.getItem(i);
273 				if (childNode.getNodeType() == Node.ELEMENT_NODE) {
274 					queue.add((Element)childNode.cast());
275 				}
276 			}
277 		}
278 		
279 		// Change all the attributes which are URI references
280 		queue.add(srcSvg.getElement());
281 		while (queue.size() > 0) {
282 			Element element = queue.remove(0);
283 			if (DOMHelper.hasAttributeNS(element, SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE)) {
284 				String idRef = DOMHelper.getAttributeNS(element, SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE);
285 				// TODO: Test will probably not work on Opera (where all URLs are made absolute)
286 				if (idRef.startsWith("#")) {
287 					// Normalize hrefs to internal elements (such as between two gradients),
288 					// not hrefs to external elements (such as between an image and its png)
289 					String normalizeIdRef = idToNormalizedId.get(idRef.substring(1));
290 					DOMHelper.setAttributeNS(element, SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, "#" + normalizeIdRef);
291 				}
292 			}
293 			NamedNodeMap<Attr> attrs = DOMHelper.getAttributes(element);
294 			for (int i = 0, length = attrs.getLength(); i < length; i++) {
295 				Attr attr = attrs.item(i);
296 				if (IDREF_ATTS.contains(attr.getName())) {
297 					StringBuilder builder = new StringBuilder();
298 					TOKENIZER.tokenize(attr.getValue());
299 					IdRefTokenizer.IdRefToken token;
300 					while ((token = TOKENIZER.nextToken()) != null) {
301 						String value = token.getValue();
302 						if (token.getKind() == IdRefTokenizer.IdRefToken.DATA) {
303 							builder.append(value);
304 						} else {
305 							value = idToNormalizedId.get(value);
306 							builder.append(value == null ? token.getValue() : value);
307 						}
308 					}
309 					attr.setValue(builder.toString());
310 				}
311 			}
312 			NodeList<Node> childNodes =  element.getChildNodes();
313 			for (int i = 0, length = childNodes.getLength(); i < length; i++) {
314 				Node childNode = childNodes.getItem(i);
315 				if (childNode.getNodeType() == Node.ELEMENT_NODE) {
316 					queue.add((Element)childNode.cast());
317 				}
318 			}
319 		}
320 	}
321 
322 	/**
323 	 * Transfers all the children for one element to another element
324 	 * @param src the source element
325 	 * @param dest the destination element
326 	 */
327 	public static void reparent(OMSVGElement src, OMSVGElement dest) {
328 		Element srcElement = src.getElement();
329 		Element destElement = dest.getElement();
330 		Node node;
331 		while((node = srcElement.getFirstChild()) != null) {
332 			destElement.appendChild(srcElement.removeChild(node));
333 		}
334 	}
335 
336 }
337