View Javadoc

1   /*
2    * VectorGraphics2D: Vector export for Java(R) Graphics2D
3    *
4    * (C) Copyright 2010 Erich Seifert <dev[at]erichseifert.de>
5    *
6    * This file is part of VectorGraphics2D.
7    *
8    * VectorGraphics2D is free software: you can redistribute it and/or modify
9    * it under the terms of the GNU Lesser General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or
11   * (at your option) any later version.
12   *
13   * VectorGraphics2D is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   * GNU Lesser General Public License for more details.
17   *
18   * You should have received a copy of the GNU Lesser General Public License
19   * along with VectorGraphics2D.  If not, see <http://www.gnu.org/licenses/>.
20   */
21  
22  package de.erichseifert.vectorgraphics2d;
23  
24  import java.awt.BasicStroke;
25  import java.awt.Color;
26  import java.awt.Image;
27  import java.awt.Shape;
28  import java.awt.geom.AffineTransform;
29  import java.awt.geom.Ellipse2D;
30  import java.awt.geom.Line2D;
31  import java.awt.geom.PathIterator;
32  import java.awt.geom.Rectangle2D;
33  import java.awt.geom.RoundRectangle2D;
34  import java.awt.image.BufferedImage;
35  import java.io.ByteArrayOutputStream;
36  import java.io.IOException;
37  import java.util.Map;
38  
39  import javax.imageio.ImageIO;
40  import javax.xml.bind.DatatypeConverter;
41  
42  /**
43   * <code>Graphics2D</code> implementation that saves all operations to a string
44   * in the <i>Scaled Vector Graphics</i> (SVG) format.
45   */
46  public class SVGGraphics2D extends VectorGraphics2D {
47  	/** Mapping of stroke endcap values from Java to SVG. */
48  	private static final Map<Integer, String> STROKE_ENDCAPS = DataUtils.map(
49  		new Integer[] { BasicStroke.CAP_BUTT, BasicStroke.CAP_ROUND, BasicStroke.CAP_SQUARE },
50  		new String[] { "butt", "round", "square" }
51  	);
52  
53  	/** Mapping of line join values for path drawing from Java to SVG. */
54  	private static final Map<Integer, String> STROKE_LINEJOIN = DataUtils.map(
55  		new Integer[] { BasicStroke.JOIN_MITER, BasicStroke.JOIN_ROUND, BasicStroke.JOIN_BEVEL },
56  		new String[] { "miter", "round", "bevel" }
57  	);
58  
59  	/** Prefix string for ids of clipping paths. */
60  	private static final String CLIP_PATH_ID = "clip";
61  	/** Number of the current clipping path. */
62  	private long clipCounter;
63  
64  	/**
65  	 * Constructor that initializes a new <code>SVGGraphics2D</code> instance.
66  	 * The document dimension must be specified as parameters.
67  	 */
68  	public SVGGraphics2D(double x, double y, double width, double height) {
69  		super(x, y, width, height);
70  		writeHeader();
71  	}
72  
73  	@Override
74  	protected void writeString(String str, double x, double y) {
75  		// Escape string
76  		str = str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
77  
78  		float fontSize = getFont().getSize2D();
79  		//float leading = getFont().getLineMetrics("", getFontRenderContext()).getLeading();
80  
81  		/*
82  		// Extract lines
83  		String[] lines = str.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
84  
85  		// Output lines
86  		writeln("<text style=\"font:", fontSize, "px ", getFont().getFamily(), "\">");
87  		for (int i = 0; i < lines.length; i++) {
88  			String line = lines[i];
89  			writeln(" <tspan x=\"", x, "\" y=\"", y + i*fontSize + ((i>0) ? leading : 0f), "\">", line, "</tspan>");
90  		}
91  		writeln("</text>");
92  		*/
93  
94  		str = str.replaceAll("[\r\n]", "");
95  		writeln("<text x=\"", x, "\" y=\"", y, "\" style=\"font:",
96  				fontSize, "px ", getFont().getFamily(), "\">", str, "</text>");
97  	}
98  
99  	@Override
100 	protected void writeImage(Image img, int imgWidth, int imgHeight, double x,
101 			double y, double width, double height) {
102 		BufferedImage bufferedImg = GraphicsUtils.toBufferedImage(img);
103 		String imgData = getSvg(bufferedImg);
104 		write("<image x=\"" , x, "\" y=\"" , y, "\" ",
105 				"width=\"" , width, "\" height=\"" , height, "\" ",
106 				"xlink:href=\"", imgData, "\" ",
107 				"/>");
108 	}
109 
110 	@Override
111 	public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
112 		write("<polygon points=\"");
113 		for (int i = 0; i < nPoints; i++) {
114 			if (i > 0) {
115 				write(" ");
116 			}
117 			write(xPoints[i], ",", yPoints[i]);
118 		}
119 		write("\" ");
120 		writeClosingDraw();
121 	}
122 
123 	@Override
124 	public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
125 		write("<polyline points=\"");
126 		for (int i = 0; i < nPoints; i++) {
127 			if (i > 0) {
128 				write(" ");
129 			}
130 			write(xPoints[i], ",", yPoints[i]);
131 		}
132 		write("\" ");
133 		writeClosingDraw();
134 	}
135 
136 	@Override
137 	public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
138 		write("<polygon points=\"");
139 		for (int i = 0; i < nPoints; i++) {
140 			if (i > 0) {
141 				write(" ");
142 			}
143 			write(xPoints[i], ",", yPoints[i]);
144 		}
145 		write("\" ");
146 		writeClosingFill();
147 	}
148 
149 	@Override
150 	public void setClip(Shape clip) {
151 		super.setClip(clip);
152 		if (getClip() != null) {
153 			writeln("<clipPath id=\"", CLIP_PATH_ID, ++clipCounter, "\">");
154 			writeShape(getClip());
155 			writeln("/>");
156 			writeln("</clipPath>");
157 		}
158 	}
159 
160 	@Override
161 	protected void setAffineTransform(AffineTransform tx) {
162 		// Close previous transformation group
163 		if (isTransformed()) {
164 			writeln("</g>");
165 		}
166 		// Set transformation matrix
167 		super.setAffineTransform(tx);
168 		// Begin new transformation group
169 		if (isTransformed()) {
170 			double[] matrix = new double[6];
171 			getTransform().getMatrix(matrix);
172 			write("<g transform=\"matrix(", DataUtils.join(" ", matrix), ") \">");
173 		}
174 	}
175 
176 	@Override
177 	protected void writeHeader() {
178 		Rectangle2D bounds = getBounds();
179 		double x = bounds.getX();
180 		double y = bounds.getY();
181 		double w = bounds.getWidth();
182 		double h = bounds.getHeight();
183 		writeln("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
184 		writeln("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" ",
185 			"\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">");
186 		writeln("<svg version=\"1.2\" xmlns=\"http://www.w3.org/2000/svg\" ",
187 			"xmlns:xlink=\"http://www.w3.org/1999/xlink\" ",
188 			"x=\"", x, "mm\" y=\"", y, "mm\" ",
189 			"width=\"", w, "mm\" height=\"", h, "mm\" " +
190 			"viewBox=\"", x, " ", y, " ", w, " ", h, "\"",
191 			">");
192 		writeln("<style type=\"text/css\"><![CDATA[");
193 		writeln("text { font:", getFont().getSize2D(), "px ", getFont().getFamily(), "; }");
194 		writeln("]]></style>");
195 	}
196 
197 	/**
198 	 * Utility method for writing a tag closing fragment for drawing operations.
199 	 */
200 	@Override
201 	protected void writeClosingDraw() {
202 		write("style=\"fill:none;stroke:", getSvg(getColor()));
203 		if (getStroke() instanceof BasicStroke) {
204 			BasicStroke s = (BasicStroke) getStroke();
205 			if (s.getLineWidth() != 1f) {
206 				write(";stroke-width:", s.getLineWidth());
207 			}
208 			if (s.getEndCap() != BasicStroke.CAP_BUTT) {
209 				write(";stroke-linecap:", STROKE_ENDCAPS.get(s.getEndCap()));
210 			}
211 			if (s.getLineJoin() != BasicStroke.JOIN_MITER) {
212 				write(";stroke-linejoin:", STROKE_LINEJOIN.get(s.getLineJoin()));
213 			}
214 			//write(";stroke-miterlimit:", s.getMiterLimit());
215 			if (s.getDashArray() != null && s.getDashArray().length>0) {
216 				write(";stroke-dasharray:", DataUtils.join(",", s.getDashArray()));
217 				write(";stroke-dashoffset:", s.getDashPhase());
218 			}
219 		}
220 		if (getClip() != null) {
221 			write("\" clip-path=\"url(#", CLIP_PATH_ID, clipCounter, ")");
222 		}
223 		writeln("\" />");
224 	}
225 
226 	/**
227 	 * Utility method for writing a tag closing fragment for filling operations.
228 	 */
229 	@Override
230 	protected void writeClosingFill() {
231 		write("style=\"fill:", getSvg(getColor()), ";stroke:none");
232 		if (getClip() != null) {
233 			write("\" clip-path=\"url(#", CLIP_PATH_ID, clipCounter, ")");
234 		}
235 		writeln("\" />");
236 	}
237 
238 	/**
239 	 * Utility method for writing an arbitrary shape to.
240 	 * It tries to translate Java2D shapes to the corresponding SVG shape tags.
241 	 */
242 	@Override
243 	protected void writeShape(Shape s) {
244 		if (s instanceof Line2D) {
245 			Line2D l = (Line2D) s;
246 			double x1 = l.getX1();
247 			double y1 = l.getY1();
248 			double x2 = l.getX2();
249 			double y2 = l.getY2();
250 			write("<line x1=\"", x1, "\" y1=\"", y1, "\" x2=\"", x2, "\" y2=\"", y2, "\" ");
251 		} else if (s instanceof Rectangle2D) {
252 			Rectangle2D r = (Rectangle2D) s;
253 			double x = r.getX();
254 			double y = r.getY();
255 			double width = r.getWidth();
256 			double height = r.getHeight();
257 			write("<rect x=\"", x, "\" y=\"", y, "\" width=\"", width, "\" height=\"", height, "\" ");
258 		} else if (s instanceof RoundRectangle2D) {
259 			RoundRectangle2D r = (RoundRectangle2D) s;
260 			double x = r.getX();
261 			double y = r.getY();
262 			double width = r.getWidth();
263 			double height = r.getHeight();
264 			double arcWidth = r.getArcWidth();
265 			double arcHeight = r.getArcHeight();
266 			write("<rect x=\"", x, "\" y=\"", y, "\" width=\"", width, "\" height=\"", height, "\" rx=\"", arcWidth, "\" ry=\"", arcHeight, "\" ");
267 		} else if (s instanceof Ellipse2D) {
268 			Ellipse2D e = (Ellipse2D) s;
269 			double x = e.getX();
270 			double y = e.getY();
271 			double rx = e.getWidth()/2.0;
272 			double ry = e.getHeight()/2.0;
273 			write("<ellipse cx=\"", x+rx, "\" cy=\"", y+ry, "\" rx=\"", rx, "\" ry=\"", ry, "\" ");
274 		} else {
275 			write("<path d=\"");
276 			PathIterator segments = s.getPathIterator(null);
277 			double[] coords = new double[6];
278 			for (int i = 0; !segments.isDone(); i++, segments.next()) {
279 				if (i > 0) {
280 					write(" ");
281 				}
282 				int segmentType = segments.currentSegment(coords);
283 				switch (segmentType) {
284 				case PathIterator.SEG_MOVETO:
285 					write("M", coords[0], ",", coords[1]);
286 					break;
287 				case PathIterator.SEG_LINETO:
288 					write("L", coords[0], ",", coords[1]);
289 					break;
290 				case PathIterator.SEG_CUBICTO:
291 					write("C", coords[0], ",", coords[1], " ", coords[2], ",", coords[3], " ", coords[4], ",", coords[5]);
292 					break;
293 				case PathIterator.SEG_QUADTO:
294 					write("Q", coords[0], ",", coords[1], " ", coords[2], ",", coords[3]);
295 					break;
296 				case PathIterator.SEG_CLOSE:
297 					write("Z");
298 					break;
299 				}
300 			}
301 			write("\" ");
302 		}
303 	}
304 
305 	private static String getSvg(Color c) {
306 		String color = "rgb(" + c.getRed() + "," + c.getGreen() + "," + c.getBlue() + ")";
307 		if (c.getAlpha() < 255) {
308 			double opacity = c.getAlpha()/255.0;
309 			color += ";opacity:" + opacity;
310 		}
311 		return color;
312 	}
313 
314 	private static String getSvg(BufferedImage bufferedImg) {
315 		ByteArrayOutputStream data = new ByteArrayOutputStream();
316 		try {
317 			ImageIO.write(bufferedImg, "png", data);
318 		} catch (IOException e) {
319 			return "";
320 		}
321 		String dataBase64 = DatatypeConverter.printBase64Binary(data.toByteArray());
322 		return "data:image/png;base64," + dataBase64;
323 	}
324 
325 	@Override
326 	protected String getFooter() {
327 		String footer = "";
328 		// Close any previous transformation groups
329 		if (isTransformed()) {
330 			footer += "</g>\n";
331 		}
332 		footer += "</svg>\n";
333 		return footer;
334 	}
335 
336 	@Override
337 	public String toString() {
338 		String doc = super.toString();
339 		doc = doc.replaceAll("<g transform=\"[^\"]*\"></g>\n", "");
340 		return doc;
341 	}
342 }