VASSAL.tools.image.svg.SVGRenderer.java Source code

Java tutorial

Introduction

Here is the source code for VASSAL.tools.image.svg.SVGRenderer.java

Source

/*
 * $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();
}