/*
* $Id$
*
* Copyright (c) 2008 by Joel Uckelman
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.tools.image.svg;
import java.awt.Dimension;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.dom.util.SAXDocumentFactory;
import org.apache.batik.dom.util.XLinkSupport;
import org.apache.batik.dom.util.XMLSupport;
import org.apache.batik.util.XMLResourceDescriptor;
import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import VASSAL.tools.image.ImageIOException;
import VASSAL.tools.image.ImageNotFoundException;
import VASSAL.tools.io.IOUtils;
/**
* Utility methods for manipulating SVG images.
*
* @author Joel Uckelman
* @since 3.1.0
*/
public class SVGImageUtils {
// NB: SAXSVGDocumentFactory isn't thread-safe, we have to synchronize on it.
protected static final SAXSVGDocumentFactory factory =
new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
private SVGImageUtils() { }
/**
* Returns the default dimensions of the SVG image.
*
* @return the image dimensions
* @throws IOException if the image cannot be read
*/
public static Dimension getImageSize(InputStream in) throws IOException {
return getImageSize("", in);
}
/**
* Returns the default dimensions of the SVG image.
*
* @return the image dimensions
* @throws IOException if the image cannot be read
*/
public static Dimension getImageSize(String name, InputStream in)
throws IOException {
// get the SVG
final Document doc;
try {
synchronized (factory) {
doc = factory.createDocument(null, in);
}
in.close();
}
catch (DOMException e) {
throw new ImageIOException(name, e);
}
catch (FileNotFoundException e) {
throw new ImageNotFoundException(name, e);
}
catch (IOException e) {
throw new ImageIOException(name, e);
}
finally {
IOUtils.closeQuietly(in);
}
// get the default image width and height
final Element root = doc.getDocumentElement();
try {
final int width = (int) (Float.parseFloat(
root.getAttributeNS(null, "width").replaceFirst("px", ""))+0.5);
final int height = (int) (Float.parseFloat(
root.getAttributeNS(null, "height").replaceFirst("px", ""))+0.5);
return new Dimension(width, height);
}
catch (NumberFormatException e) {
throw new ImageIOException(name, e);
}
}
/**
* Conducts a recursive depth-first search for external references
* in the given SVG file.
*
* @param path the path of the file to check for external references
*/
public static List<String> getExternalReferences(String path)
throws IOException {
final ArrayList<String> reflist = new ArrayList<String>();
reflist.add(path);
return getExternalReferences(path, reflist);
}
/**
* Conducts a recursive depth-first search for external references
* in the SVG file named by path. This is a helper function for
* {@link #getExternalReferences}.
*
* @param path the path of the file to check for external references
* @param known the list of references already found
*/
protected static List<String> getExternalReferences(
String path, List<String> known) throws IOException {
final HashSet<String> follow = new HashSet<String>();
final URL here = new URL("file", null, new File(path).getCanonicalPath());
Document doc = null;
try {
synchronized (factory) {
doc = factory.createDocument(here.toString());
}
}
catch (DOMException e) {
throw (IOException) new IOException().initCause(e);
}
final NodeList usenodes = doc.getElementsByTagName("use");
for (int i = 0; i < usenodes.getLength(); ++i) {
final Element e = (Element) usenodes.item(i);
final URL url = new URL(new URL(e.getBaseURI()),
XLinkSupport.getXLinkHref(e));
// balk (for now) unless file is available on our filesystem
if (url.getProtocol().equals("file")) {
final String refpath = url.getPath();
if (!known.contains(refpath)) {
follow.add(refpath);
known.add(refpath);
}
}
else {
throw new IOException("unsupported protocol '" +
url.getProtocol() + "' in xlink:href");
}
}
for (String s : follow) {
known.addAll(getExternalReferences(s, known));
}
return known;
}
/**
* Rewrites external references contained in SVG files.
*
* @param path the path of the file to be processed
*/
public static byte[] relativizeExternalReferences(String path)
throws IOException {
// use the GenericDOMImplementation here because
// SVGDOMImplementation adds unwanted attributes to SVG elements
final SAXDocumentFactory fac = new SAXDocumentFactory(
new GenericDOMImplementation(),
XMLResourceDescriptor.getXMLParserClassName());
final URL here = new URL("file", null, new File(path).getCanonicalPath());
final StringWriter sw = new StringWriter();
try {
final Document doc = fac.createDocument(here.toString());
relativizeElement(doc.getDocumentElement());
DOMUtilities.writeDocument(doc, sw);
}
catch (DOMException e) {
throw (IOException) new IOException().initCause(e);
}
sw.flush();
return sw.toString().getBytes();
}
protected static void relativizeElement(Element e) {
// work from leaves to root in each subtree
final NodeList children = e.getChildNodes();
for (int i = 0; i < children.getLength(); ++i) {
final Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE)
relativizeElement((Element) n);
}
// relativize the xlink:href attribute if there is one
if (e.hasAttributeNS(XLinkSupport.XLINK_NAMESPACE_URI, "href")) {
try {
final URL url = new URL(new URL(e.getBaseURI()),
XLinkSupport.getXLinkHref(e));
final String anchor = url.getRef();
final String name = new File(url.getPath()).getName();
XLinkSupport.setXLinkHref(e, name + '#' + anchor);
}
// FIXME: review error message
catch (MalformedURLException ex) {
// ErrorLog.warn(ex);
}
}
// remove xml:base attribute if there is one
e.removeAttributeNS(XMLSupport.XML_NAMESPACE_URI, "base");
}
}