/*
* $Id$
*
* Copyright (c) 2007-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.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.BridgeException;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.UserAgent;
import org.apache.batik.dom.svg.SAXSVGDocumentFactory;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import org.apache.batik.gvt.renderer.ImageRendererFactory;
import org.apache.batik.transcoder.SVGAbstractTranscoder;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.TranscodingHints;
import org.apache.batik.transcoder.keys.BooleanKey;
import org.apache.batik.transcoder.keys.PaintKey;
import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.commons.lang.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import VASSAL.build.GameModule;
import VASSAL.tools.image.ImageUtils;
import VASSAL.tools.io.IOUtils;
/**
* Render an SVG image to a {@link BufferedImage}.
*
* @author Joel Uckelman
* @since 3.1.0
*/
public class SVGRenderer {
private static final Logger logger =
LoggerFactory.getLogger(SVGRenderer.class);
private static final SAXSVGDocumentFactory docFactory =
new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
private static final ImageRendererFactory rendFactory =
new ConcreteImageRendererFactory();
private final Document doc;
private final float defaultW, defaultH;
private final Rasterizer r = new Rasterizer();
public SVGRenderer(URL file, InputStream in) throws IOException {
this(file.toString(), in);
}
public SVGRenderer(String file, InputStream in) throws IOException {
// load the SVG
try {
// We synchronize on docFactory becuase it does internal caching
// of the Documents it produces. This ensures that a Document is
// being modified on one thread only.
synchronized (docFactory) {
doc = docFactory.createDocument(file, in);
}
in.close();
}
catch (DOMException e) {
throw (IOException) new IOException().initCause(e);
}
finally {
IOUtils.closeQuietly(in);
}
// get the default image size
final Element root = doc.getDocumentElement();
defaultW = Float.parseFloat(
root.getAttributeNS(null, "width").replaceFirst("px", ""));
defaultH = Float.parseFloat(
root.getAttributeNS(null, "height").replaceFirst("px", ""));
}
private static final double DEGTORAD = Math.PI/180.0;
public BufferedImage render() {
return render(0.0, 1.0);
}
public BufferedImage render(double angle, double scale) {
// The renderer needs the bounds unscaled---scaling comes from the
// width and height hints.
AffineTransform px = AffineTransform.getRotateInstance(
angle*DEGTORAD, defaultW/2.0, defaultH/2.0);
r.setTransform(px);
px = new AffineTransform(px);
px.scale(scale, scale);
final Rectangle2D rect = new Rectangle2D.Float(0, 0, defaultW, defaultH);
final Rectangle2D b = px.createTransformedShape(rect).getBounds2D();
r.addTranscodingHint(Rasterizer.KEY_WIDTH, (float) b.getWidth());
r.addTranscodingHint(Rasterizer.KEY_HEIGHT, (float) b.getHeight());
try {
r.transcode(new TranscoderInput(doc), null);
return r.getBufferedImage();
}
// FIXME: review error message
catch (BridgeException e) {
logger.error("", e);
}
catch (TranscoderException e) {
logger.error("", e);
}
return null;
}
public BufferedImage render(double angle, double scale, Rectangle2D aoi) {
// The renderer needs the bounds unscaled---scaling comes from the
// width and height hints.
AffineTransform px = AffineTransform.getRotateInstance(
angle*DEGTORAD, defaultW/2.0, defaultH/2.0);
r.setTransform(px);
px = new AffineTransform(px);
px.scale(scale, scale);
final Rectangle2D rect = new Rectangle2D.Float(0, 0, defaultW, defaultH);
r.addTranscodingHint(Rasterizer.KEY_WIDTH, (float) aoi.getWidth());
r.addTranscodingHint(Rasterizer.KEY_HEIGHT, (float) aoi.getHeight());
r.addTranscodingHint(Rasterizer.KEY_AOI, aoi);
try {
r.transcode(new TranscoderInput(doc), null);
return r.getBufferedImage();
}
// FIXME: review error message
catch (BridgeException e) {
logger.error("", e);
}
catch (TranscoderException e) {
logger.error("", e);
}
return null;
}
private static class DataArchiveDocumentLoader extends DocumentLoader {
public DataArchiveDocumentLoader(UserAgent userAgent) {
super(userAgent);
}
@Override
public Document loadDocument(String uri)
throws MalformedURLException, IOException {
final String file = new File((new URL(uri)).getPath()).getName();
BufferedInputStream in = null;
try {
in = new BufferedInputStream(
GameModule.getGameModule()
.getDataArchive()
.getInputStream(file));
final Document doc = loadDocument(uri, in);
in.close();
return doc;
}
catch (DOMException e) {
throw (IOException) new IOException().initCause(e);
}
finally {
IOUtils.closeQuietly(in);
}
}
}
private static class Rasterizer extends SVGAbstractTranscoder {
private DocumentLoader docLoader;
private BufferedImage image;
private AffineTransform xform;
public Rasterizer() {
docLoader = new DataArchiveDocumentLoader(userAgent);
}
@Override
protected BridgeContext createBridgeContext() {
return new BridgeContext(userAgent, docLoader);
}
@Override
protected void transcode(Document document,
String uri,
TranscoderOutput output)
throws TranscoderException {
if (SystemUtils.IS_OS_MAC_OSX) {
final Element g = document.createElementNS(
SVGDOMImplementation.SVG_NAMESPACE_URI, "g"
);
g.setAttributeNS(null, "transform", "rotate(0.000001)");
// interpose this <g> element between <svg> and its children
final Element svg = document.getDocumentElement();
Node n = null;
while ((n = svg.getFirstChild()) != null) {
g.appendChild(n);
}
svg.appendChild(g);
}
// Sets up root, curTxf & curAoi
super.transcode(document, uri, output);
// prepare the image to be painted
int w = (int)(width+0.5);
int h = (int)(height+0.5);
// paint the SVG document using the bridge package
// create the appropriate renderer
ImageRenderer renderer = rendFactory.createStaticImageRenderer();
renderer.updateOffScreen(w, h);
if (xform != null) curTxf.concatenate(xform);
renderer.setTransform(curTxf);
renderer.setTree(this.root);
this.root = null; // We're done with it...
// now we are sure that the aoi is the image size
final Shape raoi = new Rectangle2D.Float(0, 0, width, height);
// Warning: the renderer's AOI must be in user space
try {
renderer.repaint(curTxf.createInverse().
createTransformedShape(raoi));
}
catch (NoninvertibleTransformException e) {
throw new TranscoderException(e);
}
// FIXME: is this the image we want to use?
BufferedImage rend = renderer.getOffScreen();
renderer = null; // We're done with it...
// produce an opaque image if our background color is set
final BufferedImage dest = ImageUtils.createCompatibleImage(
w, h, !hints.containsKey(KEY_BACKGROUND_COLOR)
);
final Graphics2D g2d = GraphicsUtil.createGraphics(dest);
if (hints.containsKey(KEY_BACKGROUND_COLOR)) {
final Paint bgcolor = (Paint) hints.get(KEY_BACKGROUND_COLOR);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setPaint(bgcolor);
g2d.fillRect(0, 0, w, h);
}
if (rend != null) { // might be null if the svg document is empty
g2d.drawRenderedImage(rend, new AffineTransform());
}
g2d.dispose();
rend = null; // We're done with it...
writeImage(dest, output);
}
private void writeImage(BufferedImage image, TranscoderOutput output) {
this.image = image;
}
public BufferedImage getBufferedImage() {
return image;
}
public void setTransform(AffineTransform px) {
xform = px;
}
}
public static final TranscodingHints.Key KEY_BACKGROUND_COLOR =
new PaintKey();
public static final TranscodingHints.Key KEY_FORCE_TRANSPARENT_WHITE =
new BooleanKey();
}