// Copyright 2000-2006 FreeHEP
package org.freehep.graphicsio.svg;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.Stack;
import java.util.UUID;
import java.util.zip.GZIPOutputStream;
import org.freehep.graphicsio.AbstractVectorGraphicsIO;
import org.freehep.graphicsio.FontConstants;
import org.freehep.graphicsio.ImageConstants;
import org.freehep.graphicsio.ImageGraphics2D;
import org.freehep.graphicsio.InfoConstants;
import org.freehep.graphicsio.PageConstants;
import org.freehep.util.UserProperties;
import org.freehep.util.Value;
import org.freehep.util.io.Base64OutputStream;
import org.freehep.util.io.WriterOutputStream;
import org.freehep.xml.util.XMLWriter;
import org.geogebra.common.jre.util.ScientificFormat;
import org.geogebra.common.util.Charsets;
/**
* This class implements the Scalable Vector Graphics output. SVG specifications
* can be found at http://www.w3c.org/Graphics/SVG/
*
* The current implementation is based on REC-SVG11-20030114
*
* @author Mark Donszelmann
* @version $Id: SVGGraphics2D.java,v 1.11 2009-08-17 21:44:45 murkle Exp $
*/
public class SVGGraphics2D extends AbstractVectorGraphicsIO {
public static final String VERSION_1_1 = "Version 1.1 (REC-SVG11-20030114)";
private static final String rootKey = SVGGraphics2D.class.getName();
public static final String TRANSPARENT = rootKey + "."
+ PageConstants.TRANSPARENT;
public static final String BACKGROUND = rootKey + "."
+ PageConstants.BACKGROUND;
public static final String BACKGROUND_COLOR = rootKey + "."
+ PageConstants.BACKGROUND_COLOR;
public static final String VERSION = rootKey + ".Version";
public static final String COMPRESS = rootKey + ".Binary";
/**
* use style="font-size:20" instaed of font-size="20" see
* {@link #style(java.util.Properties)} for details
*/
public static final String STYLABLE = rootKey + ".Stylable";
public static final String IMAGE_SIZE = rootKey + "."
+ ImageConstants.IMAGE_SIZE;
public static final String EXPORT_IMAGES = rootKey + ".ExportImages";
public static final String EXPORT_SUFFIX = rootKey + ".ExportSuffix";
public static final String WRITE_IMAGES_AS = rootKey + "."
+ ImageConstants.WRITE_IMAGES_AS;
public static final String FOR = rootKey + "." + InfoConstants.FOR;
public static final String TITLE = rootKey + "." + InfoConstants.TITLE;
private final BasicStroke defaultStroke = new BasicStroke();
public static final String EMBED_FONTS = rootKey + "."
+ FontConstants.EMBED_FONTS;
private SVGFontTable fontTable;
private static final UserProperties defaultProperties = new UserProperties();
static {
defaultProperties.setProperty(TRANSPARENT, true);
defaultProperties.setProperty(BACKGROUND, false);
defaultProperties.setProperty(BACKGROUND_COLOR, Color.GRAY);
defaultProperties.setProperty(VERSION, VERSION_1_1);
defaultProperties.setProperty(COMPRESS, false);
defaultProperties.setProperty(STYLABLE, false);
defaultProperties.setProperty(IMAGE_SIZE, new Dimension(0, 0)); // ImageSize
defaultProperties.setProperty(EXPORT_IMAGES, false);
defaultProperties.setProperty(EXPORT_SUFFIX, "image");
defaultProperties.setProperty(WRITE_IMAGES_AS, ImageConstants.SMALLEST);
defaultProperties.setProperty(FOR, "");
defaultProperties.setProperty(TITLE, "");
defaultProperties.setProperty(CLIP, true);
defaultProperties.setProperty(EMBED_FONTS, false);
defaultProperties.setProperty(TEXT_AS_SHAPES, true);
}
public static Properties getDefaultProperties() {
return defaultProperties;
}
public static void setDefaultProperties(Properties newProperties) {
defaultProperties.setProperties(newProperties);
}
public static final String version = "$Revision: 1.11 $";
// current filename including path
private String filename;
// The lowerleft and upper right points of the bounding box.
private int bbx, bby, bbw, bbh;
// The private writer used for this file.
private OutputStream ros;
protected PrintWriter os;
// table for gradients
Hashtable gradients = new Hashtable();
// table for textures
Hashtable textures = new Hashtable();
private final Stack closeTags = new Stack();
private int imageNumber = 0;
private Value clipNumber;
private int width, height;
/*
* =========================================================================
* = ====== | 1. Constructors & Factory Methods
* ================================
* ================================================
*/
public SVGGraphics2D(File file, Dimension size) throws IOException {
this(new FileOutputStream(file), size);
this.filename = file.getPath();
}
public SVGGraphics2D(File file, Component component) throws IOException {
this(new FileOutputStream(file), component);
this.filename = file.getPath();
}
public SVGGraphics2D(OutputStream os, Dimension size) {
super(size, false);
init(os);
width = size.width;
height = size.height;
}
public SVGGraphics2D(OutputStream os, Component component) {
super(component, false);
init(os);
width = getSize().width;
height = getSize().height;
}
private void init(OutputStream os) {
this.ros = os;
initProperties(getDefaultProperties());
this.filename = null;
this.clipNumber = new Value().set(0);
}
protected SVGGraphics2D(SVGGraphics2D graphics,
boolean doRestoreOnDispose) {
super(graphics, doRestoreOnDispose);
// Now initialize the new object.
filename = graphics.filename;
os = graphics.os;
bbx = graphics.bbx;
bby = graphics.bby;
bbw = graphics.bbw;
bbh = graphics.bbh;
gradients = graphics.gradients;
textures = graphics.textures;
clipNumber = graphics.clipNumber;
fontTable = graphics.fontTable;
}
/*
* =========================================================================
* = ====== | 2. Document Settings
* ============================================
* ====================================
*/
/**
* Get the bounding box for this image.
*/
public void setBoundingBox() {
bbx = 0;
bby = 0;
Dimension size = getSize();
bbw = size.width;
bbh = size.height;
}
/*
* =========================================================================
* = ====== | 3. Header, Trailer, Multipage & Comments
* ========================
* ========================================================
*/
/*--------------------------------------------------------------------------------
| 3.1 Header & Trailer
*--------------------------------------------------------------------------------*/
/**
* Write out the header of this SVG file.
*/
@Override
public void writeHeader() throws IOException {
ros = new BufferedOutputStream(ros);
if (isProperty(COMPRESS)) {
ros = new GZIPOutputStream(ros);
}
os = new PrintWriter(new OutputStreamWriter(ros, Charsets.UTF_8), true);
fontTable = new SVGFontTable();
// Do the bounding box calculation.
setBoundingBox();
imageNumber = 0;
// Michael Borcherds 2008-06-06
// bugfix: added encoding="ISO-8859-1"
// as the date can contain accented characters in some languages
os.println(
"<?xml version=\"1.0\" encoding=\"ISO-8859-1\" standalone=\"no\"?>");
if (getProperty(VERSION).equals(VERSION_1_1)) {
// no DTD anymore
} else {
// FIXME experimental version
}
os.println();
os.println("<svg ");
if (getProperty(VERSION).equals(VERSION_1_1)) {
os.println(" version=\"1.1\"");
os.println(" baseProfile=\"full\"");
os.println(" xmlns=\"http://www.w3.org/2000/svg\"");
os.println(" xmlns:xlink=\"http://www.w3.org/1999/xlink\"");
os.println(" xmlns:ev=\"http://www.w3.org/2001/xml-events\"");
}
writeSize(os);
os.println(" viewBox=\"" + bbx + " " + bby + " " + bbw + " " + bbh
+ "\"");
os.println(" >");
closeTags.push("</svg> <!-- bounding box -->");
os.println("<title>");
os.println(XMLWriter.normalizeText(getProperty(TITLE)));
os.println("</title>");
/*
* removed this section otherwise SVG fails validation test
* http://validator.w3.org/check
*
*
* String producer = getClass().getName(); if (!isDeviceIndependent()) {
* producer += " " + version.substring(1, version.length() - 1); }
*
* os.println("<desc>"); os.println("<Title>" +
* XMLWriter.normalizeText(getProperty(TITLE)) + "</Title>");
* os.println("<Creator>" + XMLWriter.normalizeText(getCreator()) +
* "</Creator>"); os.println("<Producer>" +
* XMLWriter.normalizeText(producer) + "</Producer>");
* os.println("<Source>" + XMLWriter.normalizeText(getProperty(FOR)) +
* "</Source>"); if (!isDeviceIndependent()) { os.println("<Date>" +
* DateFormat.getDateTimeInstance(DateFormat.FULL,
* DateFormat.FULL).format(new Date()) + "</Date>"); }
* os.println("</desc>");
*
*/
// write default stroke
os.print("<g ");
Properties style = getStrokeProperties(defaultStroke, true);
os.print(style(style));
os.println(">");
// close default settings at the end
closeTags.push("</g> <!-- default stroke -->");
}
protected void writeSize(PrintWriter os) {
int x = 0;
int y = 0;
Dimension size = getPropertyDimension(IMAGE_SIZE);
int w = size.width;
if (w <= 0) {
w = width;
}
int h = size.height;
if (h <= 0) {
h = height;
}
// pixels
os.println(" x=\"" + x + "px\"");
os.println(" y=\"" + y + "px\"");
os.println(" width=\"" + w + "px\"");
os.println(" height=\"" + h + "px\"");
}
@Override
public void writeBackground() throws IOException {
if (isProperty(TRANSPARENT)) {
setBackground(null);
} else if (isProperty(BACKGROUND)) {
setBackground(getPropertyColor(BACKGROUND_COLOR));
clearRect(0.0, 0.0, getSize().width, getSize().height);
} else {
setBackground(getComponent() != null
? getComponent().getBackground() : Color.WHITE);
clearRect(0.0, 0.0, getSize().width, getSize().height);
}
}
/**
* Writes the font definitions and calls {@link #writeGraphicsRestore()} to
* close all open XML Tags
*
* @throws IOException
*/
@Override
public void writeTrailer() throws IOException {
// write font definition
if (isProperty(EMBED_FONTS)) {
os.println("<defs>");
os.println(fontTable.toString());
os.println("</defs> <!-- font definitions -->");
}
// restor graphic
writeGraphicsRestore();
}
@Override
public void closeStream() throws IOException {
os.close();
}
/*
* =========================================================================
* = ====== | 4. Create
* ========================================================
* ========================
*/
@Override
public Graphics create() {
try {
writeGraphicsSave();
} catch (IOException e) {
handleException(e);
}
return new SVGGraphics2D(this, true);
}
@Override
public Graphics create(double x, double y, double width, double height) {
try {
writeGraphicsSave();
} catch (IOException e) {
handleException(e);
}
SVGGraphics2D graphics = new SVGGraphics2D(this, true);
// FIXME: All other drivers have a translate(x,y), clip(0,0,w,h) here
os.println("<svg x=\"" + fixedPrecision(x) + "\" " + "y=\""
+ fixedPrecision(y) + "\" " + "width=\"" + fixedPrecision(width)
+ "\" " + "height=\"" + fixedPrecision(height) + "\" " + ">");
graphics.closeTags.push("</svg> <!-- graphics context -->");
// write default stroke
os.print("<g ");
Properties style = getStrokeProperties(defaultStroke, true);
os.print(style(style));
os.println(">");
graphics.closeTags.push("</g> <!-- default stroke -->");
return graphics;
}
@Override
protected void writeGraphicsSave() throws IOException {
// not applicable
}
@Override
protected void writeGraphicsRestore() throws IOException {
while (!closeTags.empty()) {
os.println(closeTags.pop());
}
}
/*
* =========================================================================
* = ====== | 5. Drawing Methods
* ==============================================
* ==================================
*/
/* 5.1 shapes */
/* 5.1.4. shapes */
/**
* Draws the shape using the current paint as border
*
* @param shape
*/
@Override
public void draw(Shape shape) {
// others than BasicStrokes are written by its
// {@link Stroke#createStrokedShape()}
if (getStroke() instanceof BasicStroke) {
PathIterator path = shape.getPathIterator(null);
Properties style = new Properties();
if (getPaint() != null) {
style.put("stroke", hexColor(getPaint()));
style.put("stroke-opacity",
fixedPrecision(alphaColor(getPaint())));
}
// no filling
style.put("fill", "none");
style.putAll(getStrokeProperties(getStroke(), false));
writePathIterator(path, style);
} else if (getStroke() != null) {
// fill the shape created by stroke
fill(getStroke().createStrokedShape(shape));
} else {
// FIXME: do nothing or draw using defaultStroke?
fill(defaultStroke.createStrokedShape(shape));
}
}
/**
* Fills the shape without a border using the current paint
*
* @param shape
*/
@Override
public void fill(Shape shape) {
PathIterator path = shape.getPathIterator(null);
Properties style = new Properties();
if (path.getWindingRule() == PathIterator.WIND_EVEN_ODD) {
style.put("fill-rule", "evenodd");
} else {
style.put("fill-rule", "nonzero");
}
// fill with paint
if (getPaint() != null) {
style.put("fill", hexColor(getPaint()));
style.put("fill-opacity", fixedPrecision(alphaColor(getPaint())));
}
// no border
style.put("stroke", "none");
writePathIterator(path, style);
}
/**
* writes a path using {@link #getPath(java.awt.geom.PathIterator)} and the
* given style
*
* @param pi
* @param style
* Properties for <g> tag
*/
private void writePathIterator(PathIterator pi, Properties style) {
StringBuffer result = new StringBuffer();
// write style
result.append("<g ");
result.append(style(style));
result.append(">\n ");
// draw shape
result.append(getPath(pi));
appendElementTitleAndDescription(result);
// close style
result.append("\n</g> <!-- drawing style -->");
// write in a transformed context
os.println(getTransformedString(getTransform(),
getClippedString(result.toString())));
}
/* 5.2. Images */
@Override
public void copyArea(int x, int y, int width, int height, int dx, int dy) {
writeWarning(getClass()
+ ": copyArea(int, int, int, int, int, int) not implemented.");
}
@Override
protected void writeImage(RenderedImage image, AffineTransform xform,
Color bkg) throws IOException {
StringBuffer result = new StringBuffer();
result.append("<image x=\"0\" y=\"0\" " + "width=\"");
result.append(image.getWidth());
result.append("\" " + "height=\"");
result.append(image.getHeight());
result.append("\" " + "xlink:href=\"");
String writeAs = getProperty(WRITE_IMAGES_AS);
boolean isTransparent = image.getColorModel().hasAlpha()
&& (bkg == null);
byte[] pngBytes = null;
if (writeAs.equals(ImageConstants.PNG)
|| writeAs.equals(ImageConstants.SMALLEST) || isTransparent) {
ByteArrayOutputStream png = new ByteArrayOutputStream();
ImageGraphics2D.writeImage(image, "png", new Properties(), png);
png.close();
pngBytes = png.toByteArray();
}
byte[] jpgBytes = null;
if ((writeAs.equals(ImageConstants.JPG)
|| writeAs.equals(ImageConstants.SMALLEST)) && !isTransparent) {
ByteArrayOutputStream jpg = new ByteArrayOutputStream();
ImageGraphics2D.writeImage(image, "jpg", new Properties(), jpg);
jpg.close();
jpgBytes = jpg.toByteArray();
}
String encode;
byte[] imageBytes;
if (writeAs.equals(ImageConstants.PNG) || isTransparent) {
encode = "png";
imageBytes = pngBytes;
} else if (writeAs.equals(ImageConstants.JPG)) {
encode = "jpg";
imageBytes = jpgBytes;
} else {
encode = (jpgBytes.length < (0.5 * pngBytes.length)) ? "jpg"
: "png";
imageBytes = encode.equals("jpg") ? jpgBytes : pngBytes;
}
if (isProperty(EXPORT_IMAGES)) {
imageNumber++;
// create filenames
if (filename == null) {
writeWarning(
"SVG: cannot write embedded images, since SVGGraphics2D");
writeWarning(
" was created from an OutputStream rather than a File.");
return;
}
int pos = filename.lastIndexOf(File.separatorChar);
String dirName = (pos < 0) ? "" : filename.substring(0, pos + 1);
String imageName = (pos < 0) ? filename
: filename.substring(pos + 1);
imageName += "." + getProperty(EXPORT_SUFFIX) + "-" + imageNumber
+ "." + encode;
result.append(imageName);
// write the image separately
FileOutputStream imageStream = new FileOutputStream(
dirName + imageName);
imageStream.write(imageBytes);
imageStream.close();
} else {
result.append("data:image/");
result.append(encode);
result.append(";base64,");
StringWriter writer = new StringWriter();
Base64OutputStream b64 = new Base64OutputStream(
new WriterOutputStream(writer));
b64.write(imageBytes);
b64.finish();
result.append(writer.toString());
b64.close();
}
result.append("\">");
appendElementTitleAndDescription(result);
result.append("</image>");
os.println(getTransformedString(getTransform(), getClippedString(
getTransformedString(xform, result.toString()))));
}
protected void appendElementTitleAndDescription(StringBuffer ap) {
// over-ridden in SVGExtensions
}
/* 5.3. Strings */
@Override
protected void writeString(String str, double x, double y)
throws IOException {
// str = FontEncoder.getEncodedString(str, getFont().getName());
if (isProperty(EMBED_FONTS)) {
fontTable.addGlyphs(str, getFont());
}
// font transformation should _not_ transform string position
// so we draw at 0:0 and translate _before_ using
// getFont().getTransform()
// we could not just translate before and reverse translation after
// writing because the clipping area
// create font properties
Properties style = getFontProperties(getFont());
// add stroke properties
if (getPaint() != null) {
style.put("fill", hexColor(getPaint()));
style.put("fill-opacity", fixedPrecision(alphaColor(getPaint())));
} else {
style.put("fill", "none");
}
style.put("stroke", "none");
// convert tags to string values
str = XMLWriter.normalizeText(str);
// replace leading space by a0; otherwise firefox 1.5 fails
if (str.startsWith(" ")) {
str = " " + str.substring(1);
}
os.println(getTransformedString(
// general transformation
getTransform(),
// general clip
getClippedString(getTransformedString(
// text offset
new AffineTransform(1, 0, 0, 1, x, y),
getTransformedString(
// font transformation and text
getFont().getTransform(),
"<text "
// style
+ style(style)
// coordiantes
+ " x=\"0\" y=\"0\">"
// text
+ str + "</text>")))));
}
/**
* Creates the properties list for the given font. Family, size, bold
* italic, underline and strikethrough are converted.
* {@link java.awt.font.TextAttribute#SUPERSCRIPT} is handled by
* {@link java.awt.Font#getTransform()}
*
* @return properties in svg style for the font
*/
private static Properties getFontProperties(Font font) {
Properties result = new Properties();
// attribute for font properties
Map /* <TextAttribute, ?> */ attributes = font.getAttributes();
// dialog.bold -> Helvetica with TextAttribute.WEIGHT_BOLD
SVGFontTable.normalize(attributes);
// family
result.put("font-family", attributes.get(TextAttribute.FAMILY));
// weight
if (TextAttribute.WEIGHT_BOLD
.equals(attributes.get(TextAttribute.WEIGHT))) {
result.put("font-weight", "bold");
} else {
result.put("font-weight", "normal");
}
// posture
if (TextAttribute.POSTURE_OBLIQUE
.equals(attributes.get(TextAttribute.POSTURE))) {
result.put("font-style", "italic");
} else {
result.put("font-style", "normal");
}
Object ul = font.getAttributes().get(TextAttribute.UNDERLINE);
if (ul != null) {
// underline style, only supported by CSS 3
if (TextAttribute.UNDERLINE_LOW_DOTTED.equals(ul)) {
result.put("text-underline-style", "dotted");
} else if (TextAttribute.UNDERLINE_LOW_DASHED.equals(ul)) {
result.put("text-underline-style", "dashed");
} else if (TextAttribute.UNDERLINE_ON.equals(ul)) {
result.put("text-underline-style", "solid");
}
// the underline itself, supported by CSS 2
result.put("text-decoration", "underline");
}
if (font.getAttributes().get(TextAttribute.STRIKETHROUGH) != null) {
// is the property allready witten?
if (ul == null) {
result.put("text-decoration", "underline, line-through");
} else {
result.put("text-decoration", "line-through");
}
}
Float size = (Float) attributes.get(TextAttribute.SIZE);
// Michael Borcherds 20111211 null pointer fix
if (size == null) {
size = font.getSize2D();
}
result.put("font-size", fixedPrecision(size.floatValue()));
return result;
}
/*
* =========================================================================
* = ====== | 6. Transformations
* ==============================================
* ==================================
*/
@Override
protected void writeTransform(AffineTransform transform)
throws IOException {
// written when needed
}
@Override
protected void writeSetTransform(AffineTransform transform)
throws IOException {
// written when needed
}
/*
* =========================================================================
* = ====== | 7. Clipping
* ======================================================
* ==========================
*/
@Override
protected void writeClip(Shape s) throws IOException {
// written when needed
}
@Override
protected void writeSetClip(Shape s) throws IOException {
// written when needed
}
/*
* =========================================================================
* = ====== | 8. Graphics State
* ================================================
* ================================
*/
/* 8.1. stroke/linewidth */
@Override
protected void writeWidth(float width) throws IOException {
// written when needed
}
@Override
protected void writeCap(int cap) throws IOException {
// Written when needed
}
@Override
protected void writeJoin(int join) throws IOException {
// written when needed
}
@Override
protected void writeMiterLimit(float limit) throws IOException {
// written when needed
}
@Override
protected void writeDash(float[] dash, float phase) throws IOException {
// written when needed
}
/**
* return the style tag for the stroke
*
* @param s
* Stroke to convert
* @param all
* all attributes (not only the differences to defaultStroke) are
* handled
* @return corresponding style string
*/
private Properties getStrokeProperties(Stroke s, boolean all) {
Properties result = new Properties();
// only BasisStrokes are written
if (!(s instanceof BasicStroke)) {
return result;
}
BasicStroke stroke = (BasicStroke) s;
// append linecap
if (all || (stroke.getEndCap() != defaultStroke.getEndCap())) {
// append cap
switch (stroke.getEndCap()) {
default:
case BasicStroke.CAP_BUTT:
result.put("stroke-linecap", "butt");
break;
case BasicStroke.CAP_ROUND:
result.put("stroke-linecap", "round");
break;
case BasicStroke.CAP_SQUARE:
result.put("stroke-linecap", "square");
break;
}
}
// append dasharray
if (all || !Arrays.equals(stroke.getDashArray(),
defaultStroke.getDashArray())) {
if ((stroke.getDashArray() != null)
&& (stroke.getDashArray().length > 0)) {
StringBuffer array = new StringBuffer();
for (int i = 0; i < stroke.getDashArray().length; i++) {
if (i > 0) {
array.append(",");
}
// SVG does not allow dash entry to be zero (Firefox 2.0).
float dash = stroke.getDashArray()[i];
array.append(fixedPrecision(dash > 0 ? dash : 0.1));
}
result.put("stroke-dasharray", array.toString());
} else {
result.put("stroke-dasharray", "none");
}
}
if (all || (stroke.getDashPhase() != defaultStroke.getDashPhase())) {
result.put("stroke-dashoffset",
fixedPrecision(stroke.getDashPhase()));
}
// append meter limit
if (all || (stroke.getMiterLimit() != defaultStroke.getMiterLimit())) {
result.put("stroke-miterlimit",
fixedPrecision(stroke.getMiterLimit()));
}
// append join
if (all || (stroke.getLineJoin() != defaultStroke.getLineJoin())) {
switch (stroke.getLineJoin()) {
default:
case BasicStroke.JOIN_MITER:
result.put("stroke-linejoin", "miter");
break;
case BasicStroke.JOIN_ROUND:
result.put("stroke-linejoin", "round");
break;
case BasicStroke.JOIN_BEVEL:
result.put("stroke-linejoin", "bevel");
break;
}
}
// append linewidth
if (all || (stroke.getLineWidth() != defaultStroke.getLineWidth())) {
// width of 0 means thinnest line, which does not exist in SVG
if (stroke.getLineWidth() == 0) {
result.put("stroke-width", fixedPrecision(0.000001f));
} else {
result.put("stroke-width",
fixedPrecision(stroke.getLineWidth()));
}
}
return result;
}
/* 8.2. paint/color */
@Override
public void setPaintMode() {
writeWarning(getClass() + ": setPaintMode() not implemented.");
}
@Override
public void setXORMode(Color c1) {
writeWarning(getClass() + ": setXORMode(Color) not implemented.");
}
@Override
protected void writePaint(Color c) throws IOException {
// written with every draw
}
@Override
protected void writePaint(GradientPaint paint) throws IOException {
if (gradients.get(paint) == null) {
String name = "gradient-" + gradients.size();
gradients.put(paint, name);
Point2D p1 = paint.getPoint1();
Point2D p2 = paint.getPoint2();
os.println("<defs>");
os.print(" <linearGradient id=\"" + name + "\" ");
os.print("x1=\"" + fixedPrecision(p1.getX()) + "\" ");
os.print("y1=\"" + fixedPrecision(p1.getY()) + "\" ");
os.print("x2=\"" + fixedPrecision(p2.getX()) + "\" ");
os.print("y2=\"" + fixedPrecision(p2.getY()) + "\" ");
os.print("gradientUnits=\"userSpaceOnUse\" ");
os.print("spreadMethod=\""
+ ((paint.isCyclic()) ? "reflect" : "pad") + "\" ");
os.println(">");
os.println(" <stop offset=\"0\" stop-color=\""
+ hexColor(paint.getColor1()) + "\" " + "opacity-stop=\""
+ alphaColor(paint.getColor1()) + "\" />");
os.println(" <stop offset=\"1\" stop-color=\""
+ hexColor(paint.getColor2()) + "\" " + "opacity-stop=\""
+ alphaColor(paint.getColor2()) + "\" />");
os.println(" </linearGradient>");
os.println("</defs>");
}
// create style
Properties style = new Properties();
style.put("stroke", hexColor(getPaint()));
// write style
os.print("<g ");
os.print(style(style));
os.println(">");
// close color later
closeTags.push("</g> <!-- color -->");
}
@Override
protected void writePaint(TexturePaint paint) throws IOException {
if (textures.get(paint) == null) {
String name = "texture-" + textures.size();
textures.put(paint, name);
BufferedImage image = paint.getImage();
Rectangle2D rect = paint.getAnchorRect();
os.println("<defs>");
os.print(" <pattern id=\"" + name + "\" ");
os.print("x=\"0\" ");
os.print("y=\"0\" ");
os.print("width=\"" + fixedPrecision(image.getWidth()) + "\" ");
os.print("height=\"" + fixedPrecision(image.getHeight()) + "\" ");
os.print("patternUnits=\"userSpaceOnUse\" ");
os.print("patternTransform=\"matrix("
+ fixedPrecision(rect.getWidth() / image.getWidth()) + ","
+ "0.0,0.0,"
+ fixedPrecision(rect.getHeight() / image.getHeight()) + ","
+ fixedPrecision(rect.getX()) + ","
+ fixedPrecision(rect.getY()) + ")\" ");
os.println(">");
writeImage(image, null, null);
os.println(" </pattern>");
os.println("</defs>");
}
// write default stroke
os.print("<g ");
Properties style = new Properties();
style.put("stroke", hexColor(getPaint()));
os.print(style(style));
os.println(">");
// close color later
closeTags.push("</g> <!-- color -->");
}
@Override
protected void writePaint(Paint p) throws IOException {
writeWarning(getClass() + ": writePaint(Paint) not implemented for "
+ p.getClass());
}
/* 8.3. font */
@Override
protected void writeFont(Font font) throws IOException {
// written when needed
}
/*
* =========================================================================
* = ====== | 9. Auxiliary
* ====================================================
* ============================
*/
@Override
public GraphicsConfiguration getDeviceConfiguration() {
writeWarning(
getClass() + ": getDeviceConfiguration() not implemented.");
return null;
}
@Override
public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
writeWarning(getClass()
+ ": hit(Rectangle, Shape, boolean) not implemented.");
return false;
}
@Override
public void writeComment(String s) throws IOException {
os.println("<!-- " + s + " -->");
}
@Override
public String toString() {
return "SVGGraphics2D";
}
/*
* =========================================================================
* = ====== | 10. Private/Utility Methos
* ======================================
* ==========================================
*/
/**
* Encapsulates a SVG-Tag by the given transformation matrix
*
* @param t
* Transformation
* @param s
* SVG-Tag
*/
private static String getTransformedString(AffineTransform t, String s) {
StringBuffer result = new StringBuffer();
if ((t != null) && !t.isIdentity()) {
result.append("<g transform=\"matrix(");
result.append(fixedPrecision(t.getScaleX()));
result.append(", ");
result.append(fixedPrecision(t.getShearY()));
result.append(", ");
result.append(fixedPrecision(t.getShearX()));
result.append(", ");
result.append(fixedPrecision(t.getScaleY()));
result.append(", ");
result.append(fixedPrecision(t.getTranslateX()));
result.append(", ");
result.append(fixedPrecision(t.getTranslateY()));
result.append(")\">\n");
}
result.append(s);
if ((t != null) && !t.isIdentity()) {
result.append("\n</g> <!-- transform -->");
}
return result.toString();
}
/**
* Encapsulates a SVG-Tag by the current clipping area matrix
*
* @param s
* SVG-Tag
*/
private String getClippedString(String s) {
StringBuffer result = new StringBuffer();
// clipping
if (isProperty(CLIP) && (getClip() != null)) {
// SVG uses unique lip numbers, don't reset always increment them
clipNumber.set(clipNumber.getInt() + 1);
// define clip
result.append("<clipPath id=\"clip");
// result.append(clipNumber.getInt());
// need unique id so that 2 SVGs can be embedding in the same
// webpage
// http://stackoverflow.com/questions/15911717/clippath-in-multiple-svg-tags
result.append(UUID.randomUUID());
result.append("\">\n ");
result.append(getPath(getClip().getPathIterator(null)));
result.append("\n</clipPath>\n");
// use clip
result.append("<g clip-path=\"url(#clip");
result.append(clipNumber.getInt());
result.append(")\">\n");
}
// append the string
result.append(s);
// close clipping
if (isProperty(CLIP) && (getClip() != null)) {
result.append("\n</g> <!-- clip");
result.append(clipNumber.getInt());
result.append(" -->");
}
return result.toString();
}
private float alphaColor(Paint p) {
if (p instanceof Color) {
return (float) (getPrintColor((Color) p).getAlpha() / 255.0);
} else if (p instanceof GradientPaint) {
return 1.0f;
} else if (p instanceof TexturePaint) {
return 1.0f;
}
writeWarning(getClass() + ": alphaColor() not implemented for "
+ p.getClass() + ".");
return 1.0f;
}
private String hexColor(Paint p) {
if (p instanceof Color) {
return hexColor(getPrintColor((Color) p));
} else if (p instanceof GradientPaint) {
return hexColor((GradientPaint) p);
} else if (p instanceof TexturePaint) {
return hexColor((TexturePaint) p);
}
writeWarning(getClass() + ": hexColor() not implemented for "
+ p.getClass() + ".");
return "#000000";
}
private static String hexColor(Color c) {
String s1 = Integer.toHexString(c.getRed());
s1 = (s1.length() != 2) ? "0" + s1 : s1;
String s2 = Integer.toHexString(c.getGreen());
s2 = (s2.length() != 2) ? "0" + s2 : s2;
String s3 = Integer.toHexString(c.getBlue());
s3 = (s3.length() != 2) ? "0" + s3 : s3;
return "#" + s1 + s2 + s3;
}
private String hexColor(GradientPaint p) {
return "url(#" + gradients.get(p) + ")";
}
private String hexColor(TexturePaint p) {
return "url(#" + textures.get(p) + ")";
}
protected static String getPathContent(PathIterator path) {
StringBuffer result = new StringBuffer();
double[] coords = new double[6];
result.append("d=\"");
while (!path.isDone()) {
int segType = path.currentSegment(coords);
switch (segType) {
case PathIterator.SEG_MOVETO:
result.append("M ");
result.append(fixedPrecision(coords[0]));
result.append(" ");
result.append(fixedPrecision(coords[1]));
break;
case PathIterator.SEG_LINETO:
result.append("L ");
result.append(fixedPrecision(coords[0]));
result.append(" ");
result.append(fixedPrecision(coords[1]));
break;
case PathIterator.SEG_CUBICTO:
result.append("C ");
result.append(fixedPrecision(coords[0]));
result.append(" ");
result.append(fixedPrecision(coords[1]));
result.append(" ");
result.append(fixedPrecision(coords[2]));
result.append(" ");
result.append(fixedPrecision(coords[3]));
result.append(" ");
result.append(fixedPrecision(coords[4]));
result.append(" ");
result.append(fixedPrecision(coords[5]));
break;
case PathIterator.SEG_QUADTO:
result.append("Q ");
result.append(fixedPrecision(coords[0]));
result.append(" ");
result.append(fixedPrecision(coords[1]));
result.append(" ");
result.append(fixedPrecision(coords[2]));
result.append(" ");
result.append(fixedPrecision(coords[3]));
break;
case PathIterator.SEG_CLOSE:
result.append("z");
break;
}
// Move to the next segment.
path.next();
// Not needed but makes the output readable
if (!path.isDone()) {
result.append(" ");
}
}
result.append("\"");
return result.toString();
}
protected String getPath(PathIterator path) {
StringBuffer result = new StringBuffer();
result.append("<path ");
result.append(getPathContent(path));
result.append("/>");
return result.toString();
}
/**
* For a given "key -> value" property set the method creates
* style="key1:value1;key2:value2;" or key2="value2" key2="value2" depending
* on {@link STYLABLE}.
*
* @param style
* properties to convert
* @return String
*/
private String style(Properties style) {
if ((style == null) || style.isEmpty()) {
return "";
}
StringBuffer result = new StringBuffer();
// embed everything in a "style" attribute
if (isProperty(STYLABLE)) {
result.append("style=\"");
}
Enumeration keys = style.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String value = style.getProperty(key);
result.append(key);
if (isProperty(STYLABLE)) {
result.append(":");
result.append(value);
result.append(";");
} else {
result.append("=\"");
result.append(value);
result.append("\"");
if (keys.hasMoreElements()) {
result.append(" ");
}
}
}
// close the style attribute
if (isProperty(STYLABLE)) {
result.append("\"");
}
return result.toString();
}
private static ScientificFormat scientific = new ScientificFormat(5, 8,
false);
public static String fixedPrecision(double d) {
return scientific.format(d);
}
protected PrintWriter getOutputStream() {
return os;
}
}