/*
* @(#)SVGOutputFormat.java
*
* Copyright (c) 1996-2010 The authors and contributors of JHotDraw.
* You may not use, copy or modify this file, except in compliance with the
* accompanying license terms.
*/
package org.jhotdraw.samples.svg.io;
import edu.umd.cs.findbugs.annotations.Nullable;
import org.jhotdraw.gui.filechooser.ExtensionFileFilter;
import org.jhotdraw.draw.io.OutputFormat;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.geom.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;
import net.n3.nanoxml.*;
import org.jhotdraw.draw.*;
import org.jhotdraw.geom.*;
import org.jhotdraw.gui.datatransfer.*;
import org.jhotdraw.io.*;
import org.jhotdraw.samples.svg.*;
import org.jhotdraw.samples.svg.figures.*;
import static org.jhotdraw.samples.svg.SVGAttributeKeys.*;
import static org.jhotdraw.samples.svg.SVGConstants.*;
/**
* An output format for storing drawings as
* Scalable Vector Graphics SVG Tiny 1.2.
*
* @author Werner Randelshofer
* @version $Id$
*/
public class SVGOutputFormat implements OutputFormat {
/**
* This is a counter used to create the next unique identification.
*/
private int nextId;
/**
* In this hash map we store all elements to which we have assigned
* an id.
*/
private HashMap<IXMLElement, String> identifiedElements;
/**
* This element holds all definitions of the SVG file.
*/
private IXMLElement defs;
/**
* Holds the document that is currently being written.
*/
private IXMLElement document;
/**
* Maps gradients to ID's. We use this, so that we need to store
* the same gradient only once.
*/
private HashMap<Gradient, String> gradientToIDMap;
/**
* Set this to true for pretty printing.
*/
private boolean isPrettyPrint;
private static final HashMap<Integer, String> strokeLinejoinMap;
static {
strokeLinejoinMap = new HashMap<Integer, String>();
strokeLinejoinMap.put(BasicStroke.JOIN_MITER, "miter");
strokeLinejoinMap.put(BasicStroke.JOIN_ROUND, "round");
strokeLinejoinMap.put(BasicStroke.JOIN_BEVEL, "bevel");
}
private static final HashMap<Integer, String> strokeLinecapMap;
static {
strokeLinecapMap = new HashMap<Integer, String>();
strokeLinecapMap.put(BasicStroke.CAP_BUTT, "butt");
strokeLinecapMap.put(BasicStroke.CAP_ROUND, "round");
strokeLinecapMap.put(BasicStroke.CAP_SQUARE, "square");
}
/**
* Set this variable to true if values should be written with float precision
* instead with double precision.
* Float precision is less accurate then double precision, but it uses
* less storage space.
*/
private static final boolean isFloatPrecision = true;
/** Creates a new instance. */
public SVGOutputFormat() {
}
public javax.swing.filechooser.FileFilter getFileFilter() {
return new ExtensionFileFilter("Scalable Vector Graphics (SVG)", "svg");
}
public JComponent getOutputFormatAccessory() {
return null;
}
public void setPrettyPrint(boolean newValue) {
isPrettyPrint = newValue;
}
public boolean isPrettyPrint() {
return isPrettyPrint;
}
protected void writeElement(IXMLElement parent, Figure f) throws IOException {
// Write link attribute as encosing "a" element
if (f.get(LINK) != null && f.get(LINK).trim().length() > 0) {
IXMLElement aElement = parent.createElement("a");
aElement.setAttribute("xlink:href", f.get(LINK));
if (f.get(LINK_TARGET) != null && f.get(LINK).trim().length() > 0) {
aElement.setAttribute("target", f.get(LINK_TARGET));
}
parent.addChild(aElement);
parent = aElement;
}
// Write the actual element
if (f instanceof SVGEllipseFigure) {
SVGEllipseFigure ellipse = (SVGEllipseFigure) f;
if (ellipse.getWidth() == ellipse.getHeight()) {
writeCircleElement(parent, ellipse);
} else {
writeEllipseElement(parent, ellipse);
}
} else if (f instanceof SVGGroupFigure) {
writeGElement(parent, (SVGGroupFigure) f);
} else if (f instanceof SVGImageFigure) {
writeImageElement(parent, (SVGImageFigure) f);
} else if (f instanceof SVGPathFigure) {
SVGPathFigure path = (SVGPathFigure) f;
if (path.getChildCount() == 1) {
BezierFigure bezier = (BezierFigure) path.getChild(0);
boolean isLinear = true;
for (int i = 0, n = bezier.getNodeCount(); i < n; i++) {
if (bezier.getNode(i).getMask() != 0) {
isLinear = false;
break;
}
}
if (isLinear) {
if (bezier.isClosed()) {
writePolygonElement(parent, path);
} else {
if (bezier.getNodeCount() == 2) {
writeLineElement(parent, path);
} else {
writePolylineElement(parent, path);
}
}
} else {
writePathElement(parent, path);
}
} else {
writePathElement(parent, path);
}
} else if (f instanceof SVGRectFigure) {
writeRectElement(parent, (SVGRectFigure) f);
} else if (f instanceof SVGTextFigure) {
writeTextElement(parent, (SVGTextFigure) f);
} else if (f instanceof SVGTextAreaFigure) {
writeTextAreaElement(parent, (SVGTextAreaFigure) f);
} else {
System.out.println("Unable to write: " + f);
}
}
protected void writeCircleElement(IXMLElement parent, SVGEllipseFigure f) throws IOException {
parent.addChild(
createCircle(
document,
f.getX() + f.getWidth() / 2d,
f.getY() + f.getHeight() / 2d,
f.getWidth() / 2d,
f.getAttributes()));
}
protected IXMLElement createCircle(IXMLElement doc,
double cx, double cy, double r,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("circle");
writeAttribute(elem, "cx", cx, 0d);
writeAttribute(elem, "cy", cy, 0d);
writeAttribute(elem, "r", r, 0d);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected IXMLElement createG(IXMLElement doc,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("g");
writeOpacityAttribute(elem, attributes);
return elem;
}
protected IXMLElement createLinearGradient(IXMLElement doc,
double x1, double y1, double x2, double y2,
double[] stopOffsets, Color[] stopColors, double[] stopOpacities,
boolean isRelativeToFigureBounds,
AffineTransform transform) throws IOException {
IXMLElement elem = doc.createElement("linearGradient");
writeAttribute(elem, "x1", toNumber(x1), "0");
writeAttribute(elem, "y1", toNumber(y1), "0");
writeAttribute(elem, "x2", toNumber(x2), "1");
writeAttribute(elem, "y2", toNumber(y2), "0");
writeAttribute(elem, "gradientUnits",
(isRelativeToFigureBounds) ? "objectBoundingBox" : "userSpaceOnUse",
"objectBoundingBox");
writeAttribute(elem, "gradientTransform", toTransform(transform), "none");
for (int i = 0; i < stopOffsets.length; i++) {
IXMLElement stop = new XMLElement("stop");
writeAttribute(stop, "offset", toNumber(stopOffsets[i]), null);
writeAttribute(stop, "stop-color", toColor(stopColors[i]), null);
writeAttribute(stop, "stop-opacity", toNumber(stopOpacities[i]), "1");
elem.addChild(stop);
}
return elem;
}
protected IXMLElement createRadialGradient(IXMLElement doc,
double cx, double cy, double fx, double fy, double r,
double[] stopOffsets, Color[] stopColors, double[] stopOpacities,
boolean isRelativeToFigureBounds,
AffineTransform transform) throws IOException {
IXMLElement elem = doc.createElement("radialGradient");
writeAttribute(elem, "cx", toNumber(cx), "0.5");
writeAttribute(elem, "cy", toNumber(cy), "0.5");
writeAttribute(elem, "fx", toNumber(fx), toNumber(cx));
writeAttribute(elem, "fy", toNumber(fy), toNumber(cy));
writeAttribute(elem, "r", toNumber(r), "0.5");
writeAttribute(elem, "gradientUnits",
(isRelativeToFigureBounds) ? "objectBoundingBox" : "userSpaceOnUse",
"objectBoundingBox");
writeAttribute(elem, "gradientTransform", toTransform(transform), "none");
for (int i = 0; i < stopOffsets.length; i++) {
IXMLElement stop = new XMLElement("stop");
writeAttribute(stop, "offset", toNumber(stopOffsets[i]), null);
writeAttribute(stop, "stop-color", toColor(stopColors[i]), null);
writeAttribute(stop, "stop-opacity", toNumber(stopOpacities[i]), "1");
elem.addChild(stop);
}
return elem;
}
protected void writeEllipseElement(IXMLElement parent, SVGEllipseFigure f) throws IOException {
parent.addChild(createEllipse(
document,
f.getX() + f.getWidth() / 2d,
f.getY() + f.getHeight() / 2d,
f.getWidth() / 2d,
f.getHeight() / 2d,
f.getAttributes()));
}
protected IXMLElement createEllipse(IXMLElement doc,
double cx, double cy, double rx, double ry,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("ellipse");
writeAttribute(elem, "cx", cx, 0d);
writeAttribute(elem, "cy", cy, 0d);
writeAttribute(elem, "rx", rx, 0d);
writeAttribute(elem, "ry", ry, 0d);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected void writeGElement(IXMLElement parent, SVGGroupFigure f) throws IOException {
IXMLElement elem = createG(document, f.getAttributes());
for (Figure child : f.getChildren()) {
writeElement(elem, child);
}
parent.addChild(elem);
}
protected void writeImageElement(IXMLElement parent, SVGImageFigure f) throws IOException {
parent.addChild(
createImage(document,
f.getX(),
f.getY(),
f.getWidth(),
f.getHeight(),
f.getImageData(),
f.getAttributes()));
}
protected IXMLElement createImage(IXMLElement doc,
double x, double y, double w, double h,
byte[] imageData,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("image");
writeAttribute(elem, "x", x, 0d);
writeAttribute(elem, "y", y, 0d);
writeAttribute(elem, "width", w, 0d);
writeAttribute(elem, "height", h, 0d);
writeAttribute(elem, "xlink:href", "data:image;base64," + Base64.encodeBytes(imageData), "");
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected void writePathElement(IXMLElement parent, SVGPathFigure f) throws IOException {
BezierPath[] beziers = new BezierPath[f.getChildCount()];
for (int i = 0; i < beziers.length; i++) {
beziers[i] = ((BezierFigure) f.getChild(i)).getBezierPath();
}
parent.addChild(createPath(
document,
beziers,
f.getAttributes()));
}
protected IXMLElement createPath(IXMLElement doc,
BezierPath[] beziers,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("path");
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
writeAttribute(elem, "d", toPath(beziers), null);
return elem;
}
protected void writePolygonElement(IXMLElement parent, SVGPathFigure f) throws IOException {
LinkedList<Point2D.Double> points = new LinkedList<Point2D.Double>();
for (int i = 0, n = f.getChildCount(); i < n; i++) {
BezierPath bezier = ((BezierFigure) f.getChild(i)).getBezierPath();
for (BezierPath.Node node : bezier) {
points.add(new Point2D.Double(node.x[0], node.y[0]));
}
}
parent.addChild(createPolygon(
document,
points.toArray(new Point2D.Double[points.size()]),
f.getAttributes()));
}
protected IXMLElement createPolygon(IXMLElement doc,
Point2D.Double[] points,
Map<AttributeKey<?>, Object> attributes)
throws IOException {
IXMLElement elem = doc.createElement("polygon");
writeAttribute(elem, "points", toPoints(points), null);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected void writePolylineElement(IXMLElement parent, SVGPathFigure f) throws IOException {
LinkedList<Point2D.Double> points = new LinkedList<Point2D.Double>();
for (int i = 0, n = f.getChildCount(); i < n; i++) {
BezierPath bezier = ((BezierFigure) f.getChild(i)).getBezierPath();
for (BezierPath.Node node : bezier) {
points.add(new Point2D.Double(node.x[0], node.y[0]));
}
}
parent.addChild(createPolyline(
document,
points.toArray(new Point2D.Double[points.size()]),
f.getAttributes()));
}
protected IXMLElement createPolyline(IXMLElement doc,
Point2D.Double[] points,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("polyline");
writeAttribute(elem, "points", toPoints(points), null);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected void writeLineElement(IXMLElement parent, SVGPathFigure f)
throws IOException {
BezierFigure bezier = (BezierFigure) f.getChild(0);
parent.addChild(createLine(
document,
bezier.getNode(0).x[0],
bezier.getNode(0).y[0],
bezier.getNode(1).x[0],
bezier.getNode(1).y[0],
f.getAttributes()));
}
protected IXMLElement createLine(IXMLElement doc,
double x1, double y1, double x2, double y2,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("line");
writeAttribute(elem, "x1", x1, 0d);
writeAttribute(elem, "y1", y1, 0d);
writeAttribute(elem, "x2", x2, 0d);
writeAttribute(elem, "y2", y2, 0d);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected void writeRectElement(IXMLElement parent, SVGRectFigure f) throws IOException {
parent.addChild(
createRect(
document,
f.getX(),
f.getY(),
f.getWidth(),
f.getHeight(),
f.getArcWidth(),
f.getArcHeight(),
f.getAttributes()));
}
protected IXMLElement createRect(IXMLElement doc,
double x, double y, double width, double height,
double rx, double ry,
Map<AttributeKey<?>, Object> attributes)
throws IOException {
IXMLElement elem = doc.createElement("rect");
writeAttribute(elem, "x", x, 0d);
writeAttribute(elem, "y", y, 0d);
writeAttribute(elem, "width", width, 0d);
writeAttribute(elem, "height", height, 0d);
writeAttribute(elem, "rx", rx, 0d);
writeAttribute(elem, "ry", ry, 0d);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
return elem;
}
protected void writeTextElement(IXMLElement parent, SVGTextFigure f) throws IOException {
DefaultStyledDocument styledDoc = new DefaultStyledDocument();
try {
styledDoc.insertString(0, f.getText(), null);
} catch (BadLocationException e) {
InternalError error = new InternalError(e.getMessage());
error.initCause(e);
throw error;
}
parent.addChild(
createText(
document,
f.getCoordinates(),
f.getRotates(),
styledDoc,
f.getAttributes()));
}
protected IXMLElement createText(IXMLElement doc,
Point2D.Double[] coordinates, double[] rotate,
StyledDocument text,
Map<AttributeKey<?>, Object> attributes) throws IOException {
IXMLElement elem = doc.createElement("text");
StringBuilder bufX = new StringBuilder();
StringBuilder bufY = new StringBuilder();
for (int i = 0; i < coordinates.length; i++) {
if (i != 0) {
bufX.append(',');
bufY.append(',');
}
bufX.append(toNumber(coordinates[i].getX()));
bufY.append(toNumber(coordinates[i].getY()));
}
StringBuilder bufR = new StringBuilder();
if (rotate != null) {
for (int i = 0; i < rotate.length; i++) {
if (i != 0) {
bufR.append(',');
}
bufR.append(toNumber(rotate[i]));
}
}
writeAttribute(elem, "x", bufX.toString(), "0");
writeAttribute(elem, "y", bufY.toString(), "0");
writeAttribute(elem, "rotate", bufR.toString(), "");
String str;
try {
str = text.getText(0, text.getLength());
} catch (BadLocationException e) {
InternalError error = new InternalError(e.getMessage());
error.initCause(e);
throw error;
}
elem.setContent(str);
writeShapeAttributes(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeTransformAttribute(elem, attributes);
writeFontAttributes(elem, attributes);
return elem;
}
protected void writeTextAreaElement(IXMLElement parent, SVGTextAreaFigure f)
throws IOException {
DefaultStyledDocument styledDoc = new DefaultStyledDocument();
try {
styledDoc.insertString(0, f.getText(), null);
} catch (BadLocationException e) {
InternalError error = new InternalError(e.getMessage());
error.initCause(e);
throw error;
}
Rectangle2D.Double bounds = f.getBounds();
parent.addChild(
createTextArea(
document,
bounds.x, bounds.y, bounds.width, bounds.height,
styledDoc,
f.getAttributes()));
}
protected IXMLElement createTextArea(IXMLElement doc,
double x, double y, double w, double h,
StyledDocument text,
Map<AttributeKey<?>, Object> attributes)
throws IOException {
IXMLElement elem = doc.createElement("textArea");
writeAttribute(elem, "x", toNumber(x), "0");
writeAttribute(elem, "y", toNumber(y), "0");
writeAttribute(elem, "width", toNumber(w), "0");
writeAttribute(elem, "height", toNumber(h), "0");
String str;
try {
str = text.getText(0, text.getLength());
} catch (BadLocationException e) {
InternalError error = new InternalError(e.getMessage());
error.initCause(e);
throw error;
}
String[] lines = str.split("\n");
for (int i = 0; i < lines.length; i++) {
if (i != 0) {
elem.addChild(doc.createElement("tbreak"));
}
IXMLElement contentElement = doc.createElement(null);
contentElement.setContent(lines[i]);
elem.addChild(contentElement);
}
writeShapeAttributes(elem, attributes);
writeTransformAttribute(elem, attributes);
writeOpacityAttribute(elem, attributes);
writeFontAttributes(elem, attributes);
return elem;
}
// ------------
// Attributes
// ------------
/* Writes shape attributes.
*/
protected void writeShapeAttributes(IXMLElement elem, Map<AttributeKey<?>, Object> m)
throws IOException {
Color color;
String value;
int intValue;
//'color'
// Value: <color> | inherit
// Initial: depends on user agent
// Applies to: None. Indirectly affects other properties via currentColor
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: Specified <color> value, except inherit
//
// Nothing to do: Attribute 'color' is not needed.
//'color-rendering'
// Value: auto | optimizeSpeed | optimizeQuality | inherit
// Initial: auto
// Applies to: container elements , graphics elements and 'animateColor'
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: Specified value, except inherit
//
// Nothing to do: Attribute 'color-rendering' is not needed.
// 'fill'
// Value: <paint> | inherit (See Specifying paint)
// Initial: black
// Applies to: shapes and text content elements
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: "none", system paint, specified <color> value or absolute IRI
Gradient gradient = FILL_GRADIENT.get(m);
if (gradient != null) {
String id;
if (gradientToIDMap.containsKey(gradient)) {
id = gradientToIDMap.get(gradient);
} else {
IXMLElement gradientElem;
if (gradient instanceof LinearGradient) {
LinearGradient lg = (LinearGradient) gradient;
gradientElem = createLinearGradient(document,
lg.getX1(), lg.getY1(),
lg.getX2(), lg.getY2(),
lg.getStopOffsets(),
lg.getStopColors(),
lg.getStopOpacities(),
lg.isRelativeToFigureBounds(),
lg.getTransform());
} else /*if (gradient instanceof RadialGradient)*/ {
RadialGradient rg = (RadialGradient) gradient;
gradientElem = createRadialGradient(document,
rg.getCX(), rg.getCY(),
rg.getFX(), rg.getFY(),
rg.getR(),
rg.getStopOffsets(),
rg.getStopColors(),
rg.getStopOpacities(),
rg.isRelativeToFigureBounds(),
rg.getTransform());
}
id = getId(gradientElem);
gradientElem.setAttribute("id", "xml", id);
defs.addChild(gradientElem);
gradientToIDMap.put(gradient, id);
}
writeAttribute(elem, "fill", "url(#" + id + ")", "#000");
} else {
writeAttribute(elem, "fill", toColor(FILL_COLOR.get(m)), "#000");
}
//'fill-opacity'
//Value: <opacity-value> | inherit
//Initial: 1
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "fill-opacity", FILL_OPACITY.get(m), 1d);
// 'fill-rule'
// Value: nonzero | evenodd | inherit
// Initial: nonzero
// Applies to: shapes and text content elements
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: Specified value, except inherit
if (WINDING_RULE.get(m) != WindingRule.NON_ZERO) {
writeAttribute(elem, "fill-rule", "evenodd", "nonzero");
}
//'stroke'
//Value: <paint> | inherit (See Specifying paint)
//Initial: none
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: "none", system paint, specified <color> value
// or absolute IRI
gradient = STROKE_GRADIENT.get(m);
if (gradient != null) {
String id;
if (gradientToIDMap.containsKey(gradient)) {
id = gradientToIDMap.get(gradient);
} else {
IXMLElement gradientElem;
if (gradient instanceof LinearGradient) {
LinearGradient lg = (LinearGradient) gradient;
gradientElem = createLinearGradient(document,
lg.getX1(), lg.getY1(),
lg.getX2(), lg.getY2(),
lg.getStopOffsets(),
lg.getStopColors(),
lg.getStopOpacities(),
lg.isRelativeToFigureBounds(),
lg.getTransform());
} else /*if (gradient instanceof RadialGradient)*/ {
RadialGradient rg = (RadialGradient) gradient;
gradientElem = createRadialGradient(document,
rg.getCX(), rg.getCY(),
rg.getFX(), rg.getFY(),
rg.getR(),
rg.getStopOffsets(),
rg.getStopColors(),
rg.getStopOpacities(),
rg.isRelativeToFigureBounds(),
rg.getTransform());
}
id = getId(gradientElem);
gradientElem.setAttribute("id", "xml", id);
defs.addChild(gradientElem);
gradientToIDMap.put(gradient, id);
}
writeAttribute(elem, "stroke", "url(#" + id + ")", "none");
} else {
writeAttribute(elem, "stroke", toColor(STROKE_COLOR.get(m)), "none");
}
//'stroke-dasharray'
//Value: none | <dasharray> | inherit
//Initial: none
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes (non-additive)
//Computed value: Specified value, except inherit
double[] dashes = STROKE_DASHES.get(m);
if (dashes != null) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < dashes.length; i++) {
if (i != 0) {
buf.append(',');
}
buf.append(toNumber(dashes[i]));
}
writeAttribute(elem, "stroke-dasharray", buf.toString(), null);
}
//'stroke-dashoffset'
//Value: <length> | inherit
//Initial: 0
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "stroke-dashoffset", STROKE_DASH_PHASE.get(m), 0d);
//'stroke-linecap'
//Value: butt | round | square | inherit
//Initial: butt
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "stroke-linecap", strokeLinecapMap.get(STROKE_CAP.get(m)), "butt");
//'stroke-linejoin'
//Value: miter | round | bevel | inherit
//Initial: miter
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "stroke-linejoin", strokeLinejoinMap.get(STROKE_JOIN.get(m)), "miter");
//'stroke-miterlimit'
//Value: <miterlimit> | inherit
//Initial: 4
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "stroke-miterlimit", STROKE_MITER_LIMIT.get(m), 4d);
//'stroke-opacity'
//Value: <opacity-value> | inherit
//Initial: 1
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "stroke-opacity", STROKE_OPACITY.get(m), 1d);
//'stroke-width'
//Value: <length> | inherit
//Initial: 1
//Applies to: shapes and text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "stroke-width", STROKE_WIDTH.get(m), 1d);
}
/* Writes the opacity attribute.
*/
protected void writeOpacityAttribute(IXMLElement elem, Map<AttributeKey<?>, Object> m)
throws IOException {
//'opacity'
//Value: <opacity-value> | inherit
//Initial: 1
//Applies to: 'image' element
//Inherited: no
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
//<opacity-value>
//The uniform opacity setting must be applied across an entire object.
//Any values outside the range 0.0 (fully transparent) to 1.0
//(fully opaque) shall be clamped to this range.
//(See Clamping values which are restricted to a particular range.)
writeAttribute(elem, "opacity", OPACITY.get(m), 1d);
}
/* Writes the transform attribute as specified in
* http://www.w3.org/TR/SVGMobile12/coords.html#TransformAttribute
*
*/
protected void writeTransformAttribute(IXMLElement elem, Map<AttributeKey<?>, Object> a)
throws IOException {
AffineTransform t = TRANSFORM.get(a);
if (t != null) {
writeAttribute(elem, "transform", toTransform(t), "none");
}
}
/* Writes font attributes as listed in
* http://www.w3.org/TR/SVGMobile12/feature.html#Font
*/
private void writeFontAttributes(IXMLElement elem, Map<AttributeKey<?>, Object> a)
throws IOException {
String value;
double doubleValue;
// 'font-family'
// Value: [[ <family-name> |
// <generic-family> ],]* [<family-name> |
// <generic-family>] | inherit
// Initial: depends on user agent
// Applies to: text content elements
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: Specified value, except inherit
writeAttribute(elem, "font-family", FONT_FACE.get(a).getFontName(), "Dialog");
// 'font-getChildCount'
// Value: <absolute-getChildCount> | <relative-getChildCount> |
// <length> | inherit
// Initial: medium
// Applies to: text content elements
// Inherited: yes, the computed value is inherited
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: Absolute length
writeAttribute(elem, "font-size", FONT_SIZE.get(a), 0d);
// 'font-style'
// Value: normal | italic | oblique | inherit
// Initial: normal
// Applies to: text content elements
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: Specified value, except inherit
writeAttribute(elem, "font-style", (FONT_ITALIC.get(a)) ? "italic" : "normal", "normal");
//'font-variant'
//Value: normal | small-caps | inherit
//Initial: normal
//Applies to: text content elements
//Inherited: yes
//Percentages: N/A
//Media: visual
//Animatable: no
//Computed value: Specified value, except inherit
// XXX - Implement me
writeAttribute(elem, "font-variant", "normal", "normal");
// 'font-weight'
// Value: normal | bold | bolder | lighter | 100 | 200 | 300
// | 400 | 500 | 600 | 700 | 800 | 900 | inherit
// Initial: normal
// Applies to: text content elements
// Inherited: yes
// Percentages: N/A
// Media: visual
// Animatable: yes
// Computed value: one of the legal numeric values, non-numeric
// values shall be converted to numeric values according to the rules
// defined below.
writeAttribute(elem, "font-weight", (FONT_BOLD.get(a)) ? "bold" : "normal", "normal");
// Note: text-decoration is an SVG 1.1 feature
//'text-decoration'
//Value: none | [ underline || overline || line-through || blink ] | inherit
//Initial: none
//Applies to: text content elements
//Inherited: no (see prose)
//Percentages: N/A
//Media: visual
//Animatable: yes
writeAttribute(elem, "text-decoration", (FONT_UNDERLINE.get(a)) ? "underline" : "none", "none");
}
/* Writes viewport attributes.
*/
private void writeViewportAttributes(IXMLElement elem, Map<AttributeKey<?>, Object> a)
throws IOException {
Object value;
Double doubleValue;
if (VIEWPORT_WIDTH.get(a) != null && VIEWPORT_HEIGHT.get(a) != null) {
// width of the viewport
writeAttribute(elem, "width", toNumber(VIEWPORT_WIDTH.get(a)), null);
// height of the viewport
writeAttribute(elem, "height", toNumber(VIEWPORT_HEIGHT.get(a)), null);
}
//'viewport-fill'
//Value: "none" | <color> | inherit
//Initial: none
//Applies to: viewport-creating elements
//Inherited: no
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: "none" or specified <color> value, except inherit
writeAttribute(elem, "viewport-fill", toColor(VIEWPORT_FILL.get(a)), "none");
//'viewport-fill-opacity'
//Value: <opacity-value> | inherit
//Initial: 1.0
//Applies to: viewport-creating elements
//Inherited: no
//Percentages: N/A
//Media: visual
//Animatable: yes
//Computed value: Specified value, except inherit
writeAttribute(elem, "viewport-fill-opacity", VIEWPORT_FILL_OPACITY.get(a), 1.0);
}
protected void writeAttribute(IXMLElement elem, String name, String value, @Nullable String defaultValue) {
writeAttribute(elem, name, SVG_NAMESPACE, value, defaultValue);
}
protected void writeAttribute(IXMLElement elem, String name, String namespace, String value, @Nullable String defaultValue) {
if (!value.equals(defaultValue)) {
elem.setAttribute(name, value);
}
}
protected void writeAttribute(IXMLElement elem, String name, double value, double defaultValue) {
writeAttribute(elem, name, SVG_NAMESPACE, value, defaultValue);
}
protected void writeAttribute(IXMLElement elem, String name, String namespace, double value, double defaultValue) {
if (value != defaultValue) {
elem.setAttribute(name, toNumber(value));
}
}
/** Returns a value as a SVG Path attribute.
* as specified in http://www.w3.org/TR/SVGMobile12/paths.html#PathDataBNF
*/
public static String toPath(BezierPath[] paths) {
StringBuilder buf = new StringBuilder();
for (int j = 0; j < paths.length; j++) {
BezierPath path = paths[j];
if (path.size() == 0) {
// nothing to do
} else if (path.size() == 1) {
BezierPath.Node current = path.get(0);
buf.append("M ");
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
//buf.append(" L ");
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0] + 1));
} else {
BezierPath.Node previous;
BezierPath.Node current;
previous = current = path.get(0);
buf.append("M ");
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
char nextCommand = 'L';
for (int i = 1, n = path.size(); i < n; i++) {
previous = current;
current = path.get(i);
if ((previous.mask & BezierPath.C2_MASK) == 0) {
if ((current.mask & BezierPath.C1_MASK) == 0) {
if (nextCommand != 'L') {
buf.append(" L ");
nextCommand = 'L';
} else {
buf.append(' ');
}
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
} else {
if (nextCommand != 'Q') {
buf.append(" Q ");
nextCommand = 'Q';
} else {
buf.append(' ');
}
buf.append(toNumber(current.x[1]));
buf.append(' ');
buf.append(toNumber(current.y[1]));
buf.append(' ');
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
}
} else {
if ((current.mask & BezierPath.C1_MASK) == 0) {
if (nextCommand != 'Q') {
buf.append(" Q ");
nextCommand = 'Q';
} else {
buf.append(' ');
}
buf.append(toNumber(previous.x[2]));
buf.append(' ');
buf.append(toNumber(previous.y[2]));
buf.append(' ');
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
} else {
if (nextCommand != 'C') {
buf.append(" C ");
nextCommand = 'C';
} else {
buf.append(' ');
}
buf.append(toNumber(previous.x[2]));
buf.append(' ');
buf.append(toNumber(previous.y[2]));
buf.append(' ');
buf.append(toNumber(current.x[1]));
buf.append(' ');
buf.append(toNumber(current.y[1]));
buf.append(' ');
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
}
}
}
if (path.isClosed()) {
if (path.size() > 1) {
previous = path.get(path.size() - 1);
current = path.get(0);
if ((previous.mask & BezierPath.C2_MASK) == 0) {
if ((current.mask & BezierPath.C1_MASK) == 0) {
if (nextCommand != 'L') {
buf.append(" L ");
nextCommand = 'L';
} else {
buf.append(' ');
}
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
} else {
if (nextCommand != 'Q') {
buf.append(" Q ");
nextCommand = 'Q';
} else {
buf.append(' ');
}
buf.append(toNumber(current.x[1]));
buf.append(' ');
buf.append(toNumber(current.y[1]));
buf.append(' ');
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
}
} else {
if ((current.mask & BezierPath.C1_MASK) == 0) {
if (nextCommand != 'Q') {
buf.append(" Q ");
nextCommand = 'Q';
} else {
buf.append(' ');
}
buf.append(toNumber(previous.x[2]));
buf.append(' ');
buf.append(toNumber(previous.y[2]));
buf.append(' ');
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
} else {
if (nextCommand != 'C') {
buf.append(" C ");
nextCommand = 'C';
} else {
buf.append(' ');
}
buf.append(toNumber(previous.x[2]));
buf.append(' ');
buf.append(toNumber(previous.y[2]));
buf.append(' ');
buf.append(toNumber(current.x[1]));
buf.append(' ');
buf.append(toNumber(current.y[1]));
buf.append(' ');
buf.append(toNumber(current.x[0]));
buf.append(' ');
buf.append(toNumber(current.y[0]));
}
}
}
buf.append(" Z");
nextCommand = '\0';
}
}
}
return buf.toString();
}
/**
* Returns a double array as a number attribute value.
*/
public static String toNumber(double number) {
String str = (isFloatPrecision) ? Float.toString((float) number) : Double.toString(number);
if (str.endsWith(".0")) {
str = str.substring(0, str.length() - 2);
}
return str;
}
/**
* Returns a Point2D.Double array as a Points attribute value.
* as specified in http://www.w3.org/TR/SVGMobile12/shapes.html#PointsBNF
*/
public static String toPoints(Point2D.Double[] points) throws IOException {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < points.length; i++) {
if (i != 0) {
buf.append(", ");
}
buf.append(toNumber(points[i].x));
buf.append(',');
buf.append(toNumber(points[i].y));
}
return buf.toString();
}
/* Converts an AffineTransform into an SVG transform attribute value as specified in
* http://www.w3.org/TR/SVGMobile12/coords.html#TransformAttribute
*/
public static String toTransform(AffineTransform t) throws IOException {
StringBuilder buf = new StringBuilder();
switch (t.getType()) {
case AffineTransform.TYPE_IDENTITY:
buf.append("none");
break;
case AffineTransform.TYPE_TRANSLATION:
// translate(<tx> [<ty>]), specifies a translation by tx and ty.
// If <ty> is not provided, it is assumed to be zero.
buf.append("translate(");
buf.append(toNumber(t.getTranslateX()));
if (t.getTranslateY() != 0d) {
buf.append(' ');
buf.append(toNumber(t.getTranslateY()));
}
buf.append(')');
break;
/*
case AffineTransform.TYPE_GENERAL_ROTATION :
case AffineTransform.TYPE_QUADRANT_ROTATION :
case AffineTransform.TYPE_MASK_ROTATION :
// rotate(<rotate-angle> [<cx> <cy>]), specifies a rotation by
// <rotate-angle> degrees about a given point.
// If optional parameters <cx> and <cy> are not supplied, the
// rotate is about the origin of the current user coordinate
// system. The operation corresponds to the matrix
// [cos(a) sin(a) -sin(a) cos(a) 0 0].
// If optional parameters <cx> and <cy> are supplied, the rotate
// is about the point (<cx>, <cy>). The operation represents the
// equivalent of the following specification:
// translate(<cx>, <cy>) rotate(<rotate-angle>)
// translate(-<cx>, -<cy>).
buf.append("rotate(");
buf.append(toNumber(t.getScaleX()));
buf.append(')');
break;*/
case AffineTransform.TYPE_UNIFORM_SCALE:
// scale(<sx> [<sy>]), specifies a scale operation by sx
// and sy. If <sy> is not provided, it is assumed to be equal
// to <sx>.
buf.append("scale(");
buf.append(toNumber(t.getScaleX()));
buf.append(')');
break;
case AffineTransform.TYPE_GENERAL_SCALE:
case AffineTransform.TYPE_MASK_SCALE:
// scale(<sx> [<sy>]), specifies a scale operation by sx
// and sy. If <sy> is not provided, it is assumed to be equal
// to <sx>.
buf.append("scale(");
buf.append(toNumber(t.getScaleX()));
buf.append(' ');
buf.append(toNumber(t.getScaleY()));
buf.append(')');
break;
default:
// matrix(<a> <b> <c> <d> <e> <f>), specifies a transformation
// in the form of a transformation matrix of six values.
// matrix(a,b,c,d,e,f) is equivalent to applying the
// transformation matrix [a b c d e f].
buf.append("matrix(");
double[] matrix = new double[6];
t.getMatrix(matrix);
for (int i = 0; i < matrix.length; i++) {
if (i != 0) {
buf.append(' ');
}
buf.append(toNumber(matrix[i]));
}
buf.append(')');
break;
}
return buf.toString();
}
public static String toColor(Color color) {
if (color == null) {
return "none";
}
String value;
value = "000000" + Integer.toHexString(color.getRGB());
value = "#" + value.substring(value.length() - 6);
if (value.charAt(1) == value.charAt(2)
&& value.charAt(3) == value.charAt(4)
&& value.charAt(5) == value.charAt(6)) {
value = "#" + value.charAt(1) + value.charAt(3) + value.charAt(5);
}
return value;
}
@Override
public String getFileExtension() {
return "svg";
}
@Override
public void write(URI uri, Drawing drawing) throws IOException {
write(new File(uri), drawing);
}
public void write(File file, Drawing drawing) throws IOException {
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream(file));
try {
write(out, drawing);
} finally {
out.close();
}
}
@Override
public void write(OutputStream out, Drawing drawing) throws IOException {
write(out, drawing, drawing.getChildren());
}
/**
* All other write methods delegate their work to here.
*/
public void write(OutputStream out, Drawing drawing, java.util.List<Figure> figures) throws IOException {
document = new XMLElement("svg", SVG_NAMESPACE);
document.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
document.setAttribute("version", "1.2");
document.setAttribute("baseProfile", "tiny");
writeViewportAttributes(document, drawing.getAttributes());
initStorageContext(document);
defs = new XMLElement("defs");
document.addChild(defs);
for (Figure f : figures) {
writeElement(document, f);
}
// Write XML prolog
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(out, "UTF-8"));
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
// Write XML content
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.write(document, isPrettyPrint);
// Flush writer
writer.flush();
document.dispose();
}
private void initStorageContext(IXMLElement root) {
identifiedElements = new HashMap<IXMLElement, String>();
gradientToIDMap = new HashMap<Gradient, String>();
}
/**
* Gets a unique ID for the specified element.
*/
public String getId(IXMLElement element) {
if (identifiedElements.containsKey(element)) {
return identifiedElements.get(element);
} else {
String id = Integer.toString(nextId++, Character.MAX_RADIX);
identifiedElements.put(element, id);
return id;
}
}
@Override
public Transferable createTransferable(Drawing drawing, java.util.List<Figure> figures, double scaleFactor) throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
write(buf, drawing, figures);
return new InputStreamTransferable(new DataFlavor(SVG_MIMETYPE, "Image SVG"), buf.toByteArray());
}
}