de.topobyte.livecg.core.painting.backend.svg.SvgPainter.java Source code

Java tutorial

Introduction

Here is the source code for de.topobyte.livecg.core.painting.backend.svg.SvgPainter.java

Source

/* This file is part of LiveCG.
 *
 * Copyright (C) 2013  Sebastian Kuerten
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.topobyte.livecg.core.painting.backend.svg;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.imageio.ImageIO;

import noawt.java.awt.Shape;
import noawt.java.awt.geom.AffineTransform;
import noawt.java.awt.geom.Rectangle2D;

import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import de.topobyte.livecg.core.geometry.geom.Chain;
import de.topobyte.livecg.core.geometry.geom.Coordinate;
import de.topobyte.livecg.core.geometry.geom.Polygon;
import de.topobyte.livecg.core.painting.Color;
import de.topobyte.livecg.core.painting.Image;
import de.topobyte.livecg.core.painting.Painter;
import de.topobyte.livecg.core.painting.backend.ImageUtil;

public class SvgPainter implements Painter {
    final static Logger logger = LoggerFactory.getLogger(SvgPainter.class);

    private String svgNS = SVGDOMImplementation.SVG_NAMESPACE_URI;

    private Document doc;
    private Element root;
    private Element defs;

    private Color color;

    private AffineTransform transform = null;

    public SvgPainter(Document doc, Element root) {
        this.doc = doc;
        this.root = root;

        defs = doc.createElementNS(svgNS, "defs");
        root.appendChild(defs);
    }

    @Override
    public void setColor(Color color) {
        this.color = color;
    }

    @Override
    public void drawRect(int x, int y, int width, int height) {
        Element rectangle = doc.createElementNS(svgNS, "rect");
        rectangle.setAttributeNS(null, "x", Integer.toString(x));
        rectangle.setAttributeNS(null, "y", Integer.toString(y));
        rectangle.setAttributeNS(null, "width", Integer.toString(width));
        rectangle.setAttributeNS(null, "height", Integer.toString(height));
        addStrokeAttributes(rectangle);
        rectangle.setAttributeNS(null, "fill", "none");

        append(rectangle);
    }

    @Override
    public void drawRect(double x, double y, double width, double height) {
        Element rectangle = doc.createElementNS(svgNS, "rect");
        rectangle.setAttributeNS(null, "x", Double.toString(x));
        rectangle.setAttributeNS(null, "y", Double.toString(y));
        rectangle.setAttributeNS(null, "width", Double.toString(width));
        rectangle.setAttributeNS(null, "height", Double.toString(height));
        addStrokeAttributes(rectangle);
        rectangle.setAttributeNS(null, "fill", "none");

        append(rectangle);
    }

    @Override
    public void fillRect(int x, int y, int width, int height) {
        Element rectangle = doc.createElementNS(svgNS, "rect");
        rectangle.setAttributeNS(null, "x", Integer.toString(x));
        rectangle.setAttributeNS(null, "y", Integer.toString(y));
        rectangle.setAttributeNS(null, "width", Integer.toString(width));
        rectangle.setAttributeNS(null, "height", Integer.toString(height));
        rectangle.setAttributeNS(null, "fill", getCurrentColor());

        append(rectangle);
    }

    @Override
    public void fillRect(double x, double y, double width, double height) {
        Element rectangle = doc.createElementNS(svgNS, "rect");
        rectangle.setAttributeNS(null, "x", Double.toString(x));
        rectangle.setAttributeNS(null, "y", Double.toString(y));
        rectangle.setAttributeNS(null, "width", Double.toString(width));
        rectangle.setAttributeNS(null, "height", Double.toString(height));
        rectangle.setAttributeNS(null, "fill", getCurrentColor());

        append(rectangle);
    }

    @Override
    public void drawLine(int x1, int y1, int x2, int y2) {
        drawLine((double) x1, (double) y1, (double) x2, (double) y2);
    }

    @Override
    public void drawLine(double x1, double y1, double x2, double y2) {
        Element path = doc.createElementNS(svgNS, "path");
        addStrokeAttributes(path);
        path.setAttributeNS(null, "fill", "none");
        path.setAttributeNS(null, "d", String.format(Locale.US, "M %f,%f %f,%f", x1, y1, x2, y2));

        append(path);
    }

    @Override
    public void drawPath(List<Coordinate> points, boolean close) {
        if (points.size() < 2) {
            return;
        }

        SvgPathBuilder pb = new SvgPathBuilder();

        StringBuilder strb = new StringBuilder();
        Coordinate start = points.get(0);
        pb.pathMoveTo(strb, start);

        for (int i = 1; i < points.size(); i++) {
            Coordinate c = points.get(i);
            pb.pathLineTo(strb, c);
        }

        if (close) {
            pb.pathClose(strb);
        }

        stroke(strb);
    }

    @Override
    public void drawCircle(double x, double y, double radius) {
        Element circle = doc.createElementNS(svgNS, "circle");
        circle.setAttributeNS(null, "cx", Double.toString(x));
        circle.setAttributeNS(null, "cy", Double.toString(y));
        circle.setAttributeNS(null, "r", Double.toString(radius));
        circle.setAttributeNS(null, "fill", "none");
        addStrokeAttributes(circle);
        circle.setAttributeNS(null, "fill", "none");

        append(circle);
    }

    @Override
    public void fillCircle(double x, double y, double radius) {
        Element circle = doc.createElementNS(svgNS, "circle");
        circle.setAttributeNS(null, "cx", Double.toString(x));
        circle.setAttributeNS(null, "cy", Double.toString(y));
        circle.setAttributeNS(null, "r", Double.toString(radius));
        circle.setAttributeNS(null, "fill", getCurrentColor());

        append(circle);
    }

    private String getCurrentColor() {
        return String.format("#%06x", color.getRGB());
    }

    private void stroke(StringBuilder strb) {
        Element path = doc.createElementNS(svgNS, "path");
        addStrokeAttributes(path);
        path.setAttributeNS(null, "fill", "none");
        path.setAttributeNS(null, "d", strb.toString());

        append(path);
    }

    private void fill(StringBuilder strb) {
        Element path = doc.createElementNS(svgNS, "path");
        path.setAttributeNS(null, "style",
                "fill:" + getCurrentColor() + ";fill-rule:evenodd;stroke:none;fill-opacity:" + color.getAlpha());
        path.setAttributeNS(null, "d", strb.toString());

        append(path);
    }

    @Override
    public void drawPolygon(Polygon polygon) {
        Chain shell = polygon.getShell();
        drawChain(shell);
        for (Chain hole : polygon.getHoles()) {
            drawChain(hole);
        }
    }

    @Override
    public void drawChain(Chain chain) {
        if (chain.getNumberOfNodes() < 2) {
            return;
        }

        StringBuilder strb = new StringBuilder();
        appendChain(strb, chain);

        stroke(strb);
    }

    private void appendChain(StringBuilder strb, Chain chain) {
        SvgPathBuilder pb = new SvgPathBuilder();
        Coordinate start = chain.getCoordinate(0);
        pb.pathMoveTo(strb, start);

        for (int i = 1; i < chain.getNumberOfNodes(); i++) {
            Coordinate c = chain.getCoordinate(i);
            pb.pathLineTo(strb, c);
        }

        if (chain.isClosed()) {
            pb.pathClose(strb);
        }
    }

    @Override
    public void fillPolygon(Polygon polygon) {
        StringBuilder strb = new StringBuilder();

        Chain shell = polygon.getShell();
        appendChain(strb, shell);
        for (Chain hole : polygon.getHoles()) {
            strb.append(" ");
            appendChain(strb, hole);
        }

        fill(strb);
    }

    @Override
    public void draw(Shape shape) {
        SvgPathBuilder pb = new SvgPathBuilder();
        StringBuilder strb = pb.buildPath(shape);
        stroke(strb);
    }

    @Override
    public void fill(Shape shape) {
        SvgPathBuilder pb = new SvgPathBuilder();
        StringBuilder strb = pb.buildPath(shape);
        fill(strb);
    }

    @Override
    public void drawString(String text, double x, double y) {
        Element element = doc.createElementNS(svgNS, "text");
        element.setAttributeNS(null, "style", "fill:" + getCurrentColor() + ";stroke:none;fill-opacity:"
                + color.getAlpha() + ";font-family:Sans;font-size:12px");
        element.setAttributeNS(null, "x", Double.toString(x));
        element.setAttributeNS(null, "y", Double.toString(y));
        element.setTextContent(text);

        append(element);
    }

    /*
     * Clipping
     */

    private static final String CLIP_PATH_PREFIX = "clip";
    private int clipId = 1;
    private List<Integer> clipIds = null;
    private Map<Integer, Shape> clipShapes = new HashMap<Integer, Shape>();

    @Override
    public Object getClip() {
        if (clipIds == null) {
            return null;
        }
        List<Integer> copy = new ArrayList<Integer>();
        for (int i : clipIds) {
            copy.add(i);
        }
        return copy;
    }

    @Override
    public void setClip(Object clip) {
        if (clip == null) {
            clipIds = null;
        } else {
            // TODO: this could be a bug, we should copy the input clip
            // to avoid insertions into the input object
            clipIds = (List<Integer>) clip;
        }
    }

    @Override
    public void clipRect(double x, double y, double width, double height) {
        clipArea(new Rectangle2D.Double(x, y, width, height));
    }

    @Override
    public void clipArea(Shape shape) {
        int index = clipId++;
        if (clipIds == null) {
            clipIds = new ArrayList<Integer>();
        }
        clipIds.add(index);
        clipShapes.put(index, shape);
        addToDefs(index, shape);
    }

    private void addToDefs(int index, Shape shape) {
        Element clipPath = doc.createElementNS(svgNS, "clipPath");
        clipPath.setAttributeNS(null, "id", CLIP_PATH_PREFIX + index);

        SvgPathBuilder pb = new SvgPathBuilder();
        StringBuilder strb = pb.buildPath(shape);

        Element path = doc.createElementNS(svgNS, "path");
        path.setAttributeNS(null, "d", strb.toString());

        if (transform != null) {
            path.setAttributeNS(null, "transform", transformValue());
        }

        clipPath.appendChild(path);
        defs.appendChild(clipPath);
    }

    /*
     * Transformations
     */

    @Override
    public AffineTransform getTransform() {
        if (transform == null) {
            return new AffineTransform();
        }
        return new AffineTransform(transform);
    }

    @Override
    public void setTransform(AffineTransform t) {
        transform = t;
    }

    private String transformValue() {
        double[] matrix = new double[6];
        transform.getMatrix(matrix);
        StringBuilder buffer = new StringBuilder();
        buffer.append("matrix(");
        for (int i = 0; i < matrix.length; i++) {
            buffer.append(matrix[i]);
            if (i < matrix.length - 1) {
                buffer.append(" ");
            }
        }
        buffer.append(")");
        return buffer.toString();
    }

    /*
     * Appending elements to the document
     */

    private void append(Element element) {
        Element e = root;
        if (clipIds != null) {
            for (int id : clipIds) {
                Element g = doc.createElementNS(svgNS, "g");
                g.setAttributeNS(null, "clip-path", "url(#" + CLIP_PATH_PREFIX + id + ")");
                e.appendChild(g);
                e = g;
            }
        }
        if (transform != null && !transform.isIdentity()) {
            Element g = doc.createElementNS(svgNS, "g");
            g.setAttributeNS(null, "transform", transformValue());
            e.appendChild(g);
            e = g;
        }
        e.appendChild(element);
    }

    /*
     * Image embedding
     */

    private static int LINE_WIDTH = 76;

    private static String format(String text) {
        StringBuilder strb = new StringBuilder();
        String newLine = System.getProperty("line.separator");
        int length = text.length();
        int size = LINE_WIDTH;
        strb.append(newLine);
        for (int i = 0; i < length; i += size) {
            int end = i + size;
            if (end >= length) {
                end = length;
            }
            String line = text.substring(i, end);
            strb.append(line);
            strb.append(newLine);
        }
        return strb.toString();
    }

    @Override
    public void drawImage(Image image, int x, int y) {
        BufferedImage im = ImageUtil.convert(image);
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {
            boolean written = ImageIO.write(im, "png", output);
            if (!written) {
                logger.error("unable to draw image: no writer found");
            }
        } catch (IOException e) {
            logger.error("unable to draw image: " + e.getMessage());
            return;
        }
        byte[] bytes = output.toByteArray();
        String base64 = Base64.encodeBase64String(bytes);

        Element element = doc.createElementNS(svgNS, "image");
        element.setAttributeNS(null, "x", Integer.toString(x));
        element.setAttributeNS(null, "y", Integer.toString(y));
        element.setAttributeNS(null, "width", Integer.toString(image.getWidth()));
        element.setAttributeNS(null, "height", Integer.toString(image.getHeight()));
        element.setAttributeNS(null, "xlink:href", "data:image/png;base64," + base64);

        append(element);
    }

    /*
     * Stroke
     */

    private double width = 1.0;
    private float[] dash = null;
    private float phase = 0;

    private void addStrokeAttributes(Element element) {
        if (dash == null) {
            element.setAttributeNS(null, "stroke", getCurrentColor());
            element.setAttributeNS(null, "stroke-width", width + "px");
            element.setAttributeNS(null, "stroke-linecap", "round");
        } else {
            element.setAttributeNS(null, "stroke", getCurrentColor());
            element.setAttributeNS(null, "stroke-width", width + "px");
            element.setAttributeNS(null, "stroke-linejoin", "round");
            element.setAttributeNS(null, "stroke-linecap", "round");
            StringBuilder strb = new StringBuilder();
            for (int i = 0; i < dash.length; i++) {
                strb.append(dash[i]);
                if (i < dash.length - 1) {
                    strb.append(",");
                }
            }
            element.setAttributeNS(null, "stroke-dasharray", strb.toString());
            element.setAttributeNS(null, "stroke-dashoffset", "" + phase);
            element.setAttributeNS(null, "stroke-opacity", "" + color.getAlpha());
        }

    }

    @Override
    public void setStrokeWidth(double width) {
        this.width = width;
    }

    @Override
    public void setStrokeNormal() {
        dash = null;
        phase = 0;
    }

    @Override
    public void setStrokeDash(float[] dash, float phase) {
        this.dash = dash;
        this.phase = phase;
    }
}