/* ****************************************************************************** * Copyright (c) 2006-2013 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ package org.xmind.de.erichseifert.vectorgraphics2d; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Image; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map; import javax.imageio.ImageIO; import javax.xml.bind.DatatypeConverter; import org.xmind.de.erichseifert.vectorgraphics2d.util.DataUtils; import org.xmind.de.erichseifert.vectorgraphics2d.util.GraphicsUtils; /** * {@code Graphics2D} implementation that saves all operations to a string in * the <i>Scaled Vector Graphics</i> (SVG) format. * * @author Jason Wong */ public class SVGGraphics2D extends VectorGraphics2D { /** Mapping of stroke endcap values from Java to SVG. */ private static final Map<Integer, String> STROKE_ENDCAPS = DataUtils.map( new Integer[] { BasicStroke.CAP_BUTT, BasicStroke.CAP_ROUND, BasicStroke.CAP_SQUARE }, new String[] { "butt", "round", //$NON-NLS-1$ //$NON-NLS-2$ "square" }); //$NON-NLS-1$ /** Mapping of line join values for path drawing from Java to SVG. */ private static final Map<Integer, String> STROKE_LINEJOIN = DataUtils.map( new Integer[] { BasicStroke.JOIN_MITER, BasicStroke.JOIN_ROUND, BasicStroke.JOIN_BEVEL }, new String[] { "miter", "round",//$NON-NLS-1$ //$NON-NLS-2$ "bevel" });//$NON-NLS-1$ /** Prefix string for ids of clipping paths. */ private static final String CLIP_PATH_ID = "clip"; //$NON-NLS-1$ /** Number of the current clipping path. */ private long clipCounter; /** * Constructor that initializes a new {@code SVGGraphics2D} instance. The * document dimension must be specified as parameters. * * @param x * Left offset of document. * @param y * Top offset of document. * @param width * Width of document. * @param height * Height of document. */ public SVGGraphics2D(double x, double y, double width, double height) { super(x, y, width, height); writeHeader(); } @SuppressWarnings("nls") @Override protected void writeString(String str, double x, double y) { // Escape string str = str.replaceAll("&", "&").replaceAll("<", "<") .replaceAll(">", ">"); float fontSize = getFont().getSize2D(); // float leading = getFont().getLineMetrics("", // getFontRenderContext()).getLeading(); /* * // Extract lines String[] lines = str.replaceAll("\r\n", * "\n").replaceAll("\r", "\n").split("\n"); * * // Output lines writeln("<text style=\"font:", fontSize, "px ", * getFont().getFamily(), "\">"); for (int i = 0; i < lines.length; i++) * { String line = lines[i]; writeln(" <tspan x=\"", x, "\" y=\"", y + * i*fontSize + ((i>0) ? leading : 0f), "\">", line, "</tspan>"); } * writeln("</text>"); */ str = str.replaceAll("[\r\n]", ""); writeln("<text x=\"", x, "\" y=\"", y, "\" style=\"font:", fontSize, "px ", getFont().getFamily(), "\">", str, "</text>"); } @SuppressWarnings("nls") @Override protected void writeImage(Image img, int imgWidth, int imgHeight, double x, double y, double width, double height) { BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img); String imgData = getSvg(bufferedImg); write("<image x=\"", x, "\" y=\"", y, "\" ", "width=\"", width, "\" height=\"", height, "\" ", "xlink:href=\"", imgData, "\" ", "/>"); //$NON-NLS-1$ } @Override public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { Path2D s = new Path2D.Double(Path2D.WIND_NON_ZERO, xPoints.length); write("<polygon points=\""); //$NON-NLS-1$ for (int i = 0; i < nPoints; i++) { if (i == 0) { s.moveTo(xPoints[i], yPoints[i]); } else { s.lineTo(xPoints[i], yPoints[i]); write(" "); //$NON-NLS-1$ } write(xPoints[i], ",", yPoints[i]); //$NON-NLS-1$ } write("\" "); //$NON-NLS-1$ s.closePath(); writeClosingDraw(s); } @Override public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { Path2D s = new Path2D.Double(Path2D.WIND_NON_ZERO, xPoints.length); write("<polyline points=\""); //$NON-NLS-1$ for (int i = 0; i < nPoints; i++) { if (i == 0) { s.moveTo(xPoints[i], yPoints[i]); } else { s.lineTo(xPoints[i], yPoints[i]); write(" "); //$NON-NLS-1$ } write(xPoints[i], ",", yPoints[i]); //$NON-NLS-1$ } write("\" "); //$NON-NLS-1$ writeClosingDraw(s); } @Override public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { Path2D s = new Path2D.Double(Path2D.WIND_NON_ZERO, xPoints.length); write("<polygon points=\""); //$NON-NLS-1$ for (int i = 0; i < nPoints; i++) { if (i == 0) { s.moveTo(xPoints[i], yPoints[i]); } else { s.lineTo(xPoints[i], yPoints[i]); write(" "); //$NON-NLS-1$ } write(xPoints[i], ",", yPoints[i]); //$NON-NLS-1$ } write("\" "); //$NON-NLS-1$ s.closePath(); writeClosingFill(s); } @Override protected void setAffineTransform(AffineTransform tx) { if (getTransform().equals(tx)) { return; } // Close previous transformation group if (isTransformed()) { writeln("</g>"); //$NON-NLS-1$ } // Set transformation matrix super.setAffineTransform(tx); // Begin new transformation group if (isTransformed()) { double[] matrix = new double[6]; getTransform().getMatrix(matrix); writeln("<g transform=\"matrix(", DataUtils.join(" ", matrix), //$NON-NLS-1$ //$NON-NLS-2$ ") \">"); //$NON-NLS-1$ } } @SuppressWarnings("nls") @Override protected void writeHeader() { Rectangle2D bounds = getBounds(); double x = bounds.getX(); double y = bounds.getY(); double w = bounds.getWidth(); double h = bounds.getHeight(); writeln("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); writeln("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" ", "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"); writeln("<svg version=\"1.2\" xmlns=\"http://www.w3.org/2000/svg\" ", "xmlns:xlink=\"http://www.w3.org/1999/xlink\" ", "x=\"", x, "\" y=\"", y, "\" ", "width=\"", w, "\" height=\"", h, "\" " + "viewBox=\"", x, " ", y, " ", w, " ", h, "\"", ">"); writeln("<style type=\"text/css\"><![CDATA["); writeln("text { font:", getFont().getSize2D(), "px ", getFont() .getFamily(), "; }"); writeln("]]></style>"); } @Override protected void writeClosingDraw(Shape s) { write("style=\"fill:none;stroke:", getSvg(getColor())); //$NON-NLS-1$ if (getStroke() instanceof BasicStroke) { BasicStroke stroke = (BasicStroke) getStroke(); if (stroke.getLineWidth() != 1f) { write(";stroke-width:", stroke.getLineWidth()); //$NON-NLS-1$ } if (stroke.getEndCap() != BasicStroke.CAP_BUTT) { write(";stroke-linecap:", //$NON-NLS-1$ STROKE_ENDCAPS.get(stroke.getEndCap())); } if (stroke.getLineJoin() != BasicStroke.JOIN_MITER) { write(";stroke-linejoin:", //$NON-NLS-1$ STROKE_LINEJOIN.get(stroke.getLineJoin())); } // write(";stroke-miterlimit:", s.getMiterLimit()); if (stroke.getDashArray() != null && stroke.getDashArray().length > 0) { write(";stroke-dasharray:", //$NON-NLS-1$ DataUtils.join(",", stroke.getDashArray())); //$NON-NLS-1$ write(";stroke-dashoffset:", stroke.getDashPhase()); //$NON-NLS-1$ } } if (getClip() != null) { write("\" clip-path=\"url(#", getClipId(), ")"); //$NON-NLS-1$ //$NON-NLS-2$ } writeln("\" />"); //$NON-NLS-1$ } @Override protected void writeClosingFill(Shape s) { if (getPaint() instanceof Color) { write("style=\"fill:", getSvg(getColor()), ";stroke:none"); //$NON-NLS-1$ //$NON-NLS-2$ if (getClip() != null) { write("\" clip-path=\"url(#", getClipId(), ")"); //$NON-NLS-1$ //$NON-NLS-2$ } writeln("\" />"); //$NON-NLS-1$ } else { write("style=\"stroke:none\" />"); //$NON-NLS-1$ super.writeClosingFill(s); } } @Override protected void writeShape(Shape s) { writeClip(); writeUnclippedShape(s); } /** * Returns the id of the current clipping path. * * @return id string of the current clipping path. */ private String getClipId() { return CLIP_PATH_ID + clipCounter; } /** * Generates a new id for a clipping path. * * @return id string of the next clipping path. */ private String nextClipId() { clipCounter++; return getClipId(); } /** * Writes the current clipping path. */ private void writeClip() { Shape clip = getClip(); if (clip == null) { return; } write("<clipPath id=\"", nextClipId(), "\">"); //$NON-NLS-1$ //$NON-NLS-2$ writeUnclippedShape(clip); write("/>"); //$NON-NLS-1$ writeln("</clipPath>"); //$NON-NLS-1$ } /** * Writes the specified shape without clipping path information. * * @param s * Shape to be written. */ @SuppressWarnings("nls") private void writeUnclippedShape(Shape s) { if (s instanceof Line2D) { Line2D l = (Line2D) s; double x1 = l.getX1(); double y1 = l.getY1(); double x2 = l.getX2(); double y2 = l.getY2(); write("<line x1=\"", x1, "\" y1=\"", y1, "\" x2=\"", x2, "\" y2=\"", y2, "\" "); } else if (s instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; double x = r.getX(); double y = r.getY(); double width = r.getWidth(); double height = r.getHeight(); write("<rect x=\"", x, "\" y=\"", y, "\" width=\"", width, "\" height=\"", height, "\" "); } else if (s instanceof RoundRectangle2D) { RoundRectangle2D r = (RoundRectangle2D) s; double x = r.getX(); double y = r.getY(); double width = r.getWidth(); double height = r.getHeight(); double arcWidth = r.getArcWidth(); double arcHeight = r.getArcHeight(); write("<rect x=\"", x, "\" y=\"", y, "\" width=\"", width, "\" height=\"", height, "\" rx=\"", arcWidth, "\" ry=\"", arcHeight, "\" "); //$NON-NLS-1$ } else if (s instanceof Ellipse2D) { Ellipse2D e = (Ellipse2D) s; double x = e.getX(); double y = e.getY(); double rx = e.getWidth() / 2.0; double ry = e.getHeight() / 2.0; write("<ellipse cx=\"", x + rx, "\" cy=\"", y + ry, "\" rx=\"", rx, "\" ry=\"", ry, "\" "); } else { write("<path d=\""); writePath(s); write("\" "); } } /** * Writes the beginning of the specified shape without any closing commands. * * @param s * Shape to be written. */ @SuppressWarnings("nls") protected void writePath(Shape s) { PathIterator segments = s.getPathIterator(null); double[] coords = new double[6]; for (int i = 0; !segments.isDone(); i++, segments.next()) { if (i > 0) { write(" "); } int segmentType = segments.currentSegment(coords); switch (segmentType) { case PathIterator.SEG_MOVETO: write("M", coords[0], ",", coords[1]); break; case PathIterator.SEG_LINETO: write("L", coords[0], ",", coords[1]); break; case PathIterator.SEG_CUBICTO: write("C", coords[0], ",", coords[1], " ", coords[2], ",", coords[3], " ", coords[4], ",", coords[5]); break; case PathIterator.SEG_QUADTO: write("Q", coords[0], ",", coords[1], " ", coords[2], ",", coords[3]); break; case PathIterator.SEG_CLOSE: write("Z"); break; default: throw new IllegalStateException("Unknown path operation."); } } } /** * Converts a {@code Color} object to an SVG color statement. * * @param c * Color object. * @return String representation in SVG compatible format. */ private static String getSvg(Color c) { String color = "rgb(" + c.getRed() + "," + c.getGreen() + "," //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + c.getBlue() + ")"; //$NON-NLS-1$ if (c.getAlpha() < 255) { double opacity = c.getAlpha() / 255.0; color += ";opacity:" + opacity; //$NON-NLS-1$ } return color; } /** * Converts a {@code BufferedImage} object to an SVG base64 encoded string. * * @param bufferedImg * Image object. * @return String representation in SVG base64 format. */ private static String getSvg(BufferedImage bufferedImg) { ByteArrayOutputStream data = new ByteArrayOutputStream(); try { ImageIO.write(bufferedImg, "png", data); //$NON-NLS-1$ } catch (IOException e) { return ""; //$NON-NLS-1$ } String dataBase64 = DatatypeConverter.printBase64Binary(data .toByteArray()); return "data:image/png;base64," + dataBase64; //$NON-NLS-1$ } @Override protected String getFooter() { String footer = ""; //$NON-NLS-1$ // Close any previous transformation groups if (isTransformed()) { footer += "</g>\n"; //$NON-NLS-1$ } footer += "</svg>\n"; //$NON-NLS-1$ return footer; } @Override public String toString() { String doc = super.toString(); doc = doc.replaceAll("<g transform=\"[^\"]*\">\n*</g>\n", ""); //$NON-NLS-1$ //$NON-NLS-2$ return doc; } }