// 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; } }