View Javadoc

1   // Copyright 2000-2007 FreeHEP
2   package org.freehep.graphicsio.svg;
3   
4   import java.awt.BasicStroke;
5   import java.awt.Color;
6   import java.awt.Component;
7   import java.awt.Dimension;
8   import java.awt.Font;
9   import java.awt.GradientPaint;
10  import java.awt.Graphics;
11  import java.awt.GraphicsConfiguration;
12  import java.awt.Paint;
13  import java.awt.Shape;
14  import java.awt.Stroke;
15  import java.awt.TexturePaint;
16  import java.awt.font.TextAttribute;
17  import java.awt.geom.AffineTransform;
18  import java.awt.geom.GeneralPath;
19  import java.awt.geom.PathIterator;
20  import java.awt.geom.Point2D;
21  import java.awt.geom.Rectangle2D;
22  import java.awt.image.RenderedImage;
23  import java.io.BufferedOutputStream;
24  import java.io.File;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.OutputStream;
28  import java.io.PrintWriter;
29  import java.io.StringWriter;
30  import java.text.DateFormat;
31  import java.text.DecimalFormat;
32  import java.text.DecimalFormatSymbols;
33  import java.util.Arrays;
34  import java.util.Date;
35  import java.util.Enumeration;
36  import java.util.Hashtable;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.Properties;
40  import java.util.Stack;
41  import java.util.zip.GZIPOutputStream;
42  
43  import org.freehep.graphics2d.font.FontUtilities;
44  import org.freehep.graphicsio.AbstractVectorGraphicsIO;
45  import org.freehep.graphicsio.FontConstants;
46  import org.freehep.graphicsio.ImageConstants;
47  import org.freehep.graphicsio.ImageGraphics2D;
48  import org.freehep.graphicsio.InfoConstants;
49  import org.freehep.graphicsio.PageConstants;
50  import org.freehep.util.UserProperties;
51  import org.freehep.util.Value;
52  import org.freehep.util.io.Base64OutputStream;
53  import org.freehep.util.io.WriterOutputStream;
54  import org.freehep.xml.util.XMLWriter;
55  
56  /**
57   * This class implements the Scalable Vector Graphics output. SVG specifications
58   * can be found at http://www.w3c.org/Graphics/SVG/
59   *
60   * The current implementation is based on REC-SVG11-20030114
61   *
62   * @author Mark Donszelmann
63   * @version $Id: SVGGraphics2D.java 12753 2007-06-12 22:32:31Z duns $
64   */
65  public class SVGGraphics2D extends AbstractVectorGraphicsIO {
66  
67      public static final String VERSION_1_1 = "Version 1.1 (REC-SVG11-20030114)";
68  
69      private static final String rootKey = SVGGraphics2D.class.getName();
70  
71      public static final String TRANSPARENT = rootKey + "."
72              + PageConstants.TRANSPARENT;
73  
74      public static final String BACKGROUND = rootKey + "."
75              + PageConstants.BACKGROUND;
76  
77      public static final String BACKGROUND_COLOR = rootKey + "."
78              + PageConstants.BACKGROUND_COLOR;
79  
80      public static final String VERSION = rootKey + ".Version";
81  
82      public static final String COMPRESS = rootKey + ".Binary";
83  
84      /**
85       * use style="font-size:20" instaed of font-size="20"
86       * see {@link #style(java.util.Properties)} for details
87       */
88      public static final String STYLABLE = rootKey + ".Stylable";
89  
90      public static final String IMAGE_SIZE = rootKey + "."
91              + ImageConstants.IMAGE_SIZE;
92  
93      public static final String EXPORT_IMAGES = rootKey + ".ExportImages";
94  
95      public static final String EXPORT_SUFFIX = rootKey + ".ExportSuffix";
96  
97      public static final String WRITE_IMAGES_AS = rootKey + "."
98              + ImageConstants.WRITE_IMAGES_AS;
99  
100     public static final String FOR = rootKey + "." + InfoConstants.FOR;
101 
102     public static final String TITLE = rootKey + "." + InfoConstants.TITLE;
103 
104     private BasicStroke defaultStroke = new BasicStroke();
105 
106     public static final String EMBED_FONTS = rootKey + "."
107             + FontConstants.EMBED_FONTS;
108 
109     private SVGFontTable fontTable;
110 
111     private static final UserProperties defaultProperties = new UserProperties();
112     static {
113         defaultProperties.setProperty(TRANSPARENT, true);
114         defaultProperties.setProperty(BACKGROUND, false);
115         defaultProperties.setProperty(BACKGROUND_COLOR, Color.GRAY);
116 
117         defaultProperties.setProperty(VERSION, VERSION_1_1);
118         defaultProperties.setProperty(COMPRESS, false);
119 
120         defaultProperties.setProperty(STYLABLE, false);
121 
122         defaultProperties.setProperty(IMAGE_SIZE, new Dimension(0, 0)); // ImageSize
123 
124         defaultProperties.setProperty(EXPORT_IMAGES, false);
125         defaultProperties.setProperty(EXPORT_SUFFIX, "image");
126 
127         defaultProperties.setProperty(WRITE_IMAGES_AS, ImageConstants.SMALLEST);
128 
129         defaultProperties.setProperty(FOR, "");
130         defaultProperties.setProperty(TITLE, "");
131 
132         defaultProperties.setProperty(CLIP, true);
133 
134         defaultProperties.setProperty(EMBED_FONTS, false);
135         defaultProperties.setProperty(TEXT_AS_SHAPES, true);
136     }
137 
138     public static Properties getDefaultProperties() {
139         return defaultProperties;
140     }
141 
142     public static void setDefaultProperties(Properties newProperties) {
143         defaultProperties.setProperties(newProperties);
144     }
145 
146     public static final String version = "$Revision: 12753 $";
147 
148     // current filename including path
149     private String filename;
150 
151     // The lowerleft and upper right points of the bounding box.
152     private int bbx, bby, bbw, bbh;
153 
154     // The private writer used for this file.
155     private OutputStream ros;
156 
157     private PrintWriter os;
158 
159     // table for gradients
160     Hashtable gradients = new Hashtable();
161 
162     // table for textures
163     Hashtable textures = new Hashtable();
164 
165     private Stack closeTags = new Stack();
166 
167     private int imageNumber = 0;
168 
169     private Value clipNumber;
170 
171     private int width, height;
172 
173     /*
174      * ================================================================================ |
175      * 1. Constructors & Factory Methods
176      * ================================================================================
177      */
178     public SVGGraphics2D(File file, Dimension size) throws IOException {
179         this(new FileOutputStream(file), size);
180         this.filename = file.getPath();
181     }
182 
183     public SVGGraphics2D(File file, Component component) throws IOException {
184         this(new FileOutputStream(file), component);
185         this.filename = file.getPath();
186     }
187 
188     public SVGGraphics2D(OutputStream os, Dimension size) {
189         super(size, false);
190         init(os);
191         width = size.width;
192         height = size.height;
193     }
194 
195     public SVGGraphics2D(OutputStream os, Component component) {
196         super(component, false);
197         init(os);
198         width = getSize().width;
199         height = getSize().height;
200     }
201 
202     private void init(OutputStream os) {
203         this.ros = os;
204         initProperties(getDefaultProperties());
205 
206         this.filename = null;
207 
208         this.clipNumber = new Value().set(0);
209     }
210 
211     protected SVGGraphics2D(SVGGraphics2D graphics, boolean doRestoreOnDispose) {
212         super(graphics, doRestoreOnDispose);
213         // Now initialize the new object.
214         filename = graphics.filename;
215         os = graphics.os;
216         bbx = graphics.bbx;
217         bby = graphics.bby;
218         bbw = graphics.bbw;
219         bbh = graphics.bbh;
220         gradients = graphics.gradients;
221         textures = graphics.textures;
222         clipNumber = graphics.clipNumber;
223         fontTable = graphics.fontTable;
224     }
225 
226     /*
227      * ================================================================================ |
228      * 2. Document Settings
229      * ================================================================================
230      */
231 
232     /**
233      * Get the bounding box for this image.
234      */
235     public void setBoundingBox() {
236         bbx = 0;
237         bby = 0;
238 
239         Dimension size = getSize();
240         bbw = size.width;
241         bbh = size.height;
242     }
243 
244     /*
245      * ================================================================================ |
246      * 3. Header, Trailer, Multipage & Comments
247      * ================================================================================
248      */
249 
250     /*--------------------------------------------------------------------------------
251      | 3.1 Header & Trailer
252      *--------------------------------------------------------------------------------*/
253     /**
254      * Write out the header of this SVG file.
255      */
256     public void writeHeader() throws IOException {
257         ros = new BufferedOutputStream(ros);
258         if (isProperty(COMPRESS)) {
259             ros = new GZIPOutputStream(ros);
260         }
261 
262         os = new PrintWriter(ros, true);
263         fontTable = new SVGFontTable();
264 
265         // Do the bounding box calculation.
266         setBoundingBox();
267         imageNumber = 0;
268 
269         os.println("<?xml version=\"1.0\" standalone=\"no\"?>");
270         if (getProperty(VERSION).equals(VERSION_1_1)) {
271             // no DTD anymore
272         } else {
273             // FIXME experimental version
274         }
275         os.println();
276 
277         int x = 0;
278         int y = 0;
279         Dimension size = getPropertyDimension(IMAGE_SIZE);
280         int w = size.width;
281         if (w <= 0)
282             w = width;
283         int h = size.height;
284         if (h <= 0)
285             h = height;
286 
287         os.println("<svg ");
288         if (getProperty(VERSION).equals(VERSION_1_1)) {
289             os.println("     version=\"1.1\"");
290             os.println("     baseProfile=\"full\"");
291             os.println("     xmlns=\"http://www.w3.org/2000/svg\"");
292             os.println("     xmlns:xlink=\"http://www.w3.org/1999/xlink\"");
293             os.println("     xmlns:ev=\"http://www.w3.org/2001/xml-events\"");
294         }
295         os.println("     x=\"" + x + "px\"");
296         os.println("     y=\"" + y + "px\"");
297         os.println("     width=\"" + w + "px\"");
298         os.println("     height=\"" + h + "px\"");
299         os.println("     viewBox=\"" + bbx + " " + bby + " " + bbw + " " + bbh
300                 + "\"");
301         os.println("     >");
302         closeTags.push("</svg> <!-- bounding box -->");
303 
304         os.print("<title>");
305         os.print(XMLWriter.normalizeText(getProperty(TITLE)));
306         os.println("</title>");
307 
308         String producer = getClass().getName();
309         if (!isDeviceIndependent()) {
310             producer += " " + version.substring(1, version.length() - 1);
311         }
312 
313         os.print("<desc>");
314         os.print("Creator: " + XMLWriter.normalizeText(getCreator()));
315         os.print(" Producer: " + XMLWriter.normalizeText(producer));
316         os.print(" Source: " + XMLWriter.normalizeText(getProperty(FOR)));
317         if (!isDeviceIndependent()) {
318             os.print(" Date: "
319                     + DateFormat.getDateTimeInstance(DateFormat.FULL,
320                             DateFormat.FULL).format(new Date()));
321         }
322         os.println("</desc>");
323 
324         // write default stroke
325         os.print("<g ");
326         Properties style = getStrokeProperties(defaultStroke,  true);
327         os.print(style(style));
328         os.println(">");
329 
330         // close default settings at the end
331         closeTags.push("</g> <!-- default stroke -->");
332     }
333 
334     public void writeBackground() throws IOException {
335         if (isProperty(TRANSPARENT)) {
336             setBackground(null);
337         } else if (isProperty(BACKGROUND)) {
338             setBackground(getPropertyColor(BACKGROUND_COLOR));
339             clearRect(0.0, 0.0, getSize().width, getSize().height);
340         } else {
341             setBackground(getComponent() != null ? getComponent()
342                     .getBackground() : Color.WHITE);
343             clearRect(0.0, 0.0, getSize().width, getSize().height);
344         }
345     }
346 
347     /**
348      * Writes the font definitions and calls {@link #writeGraphicsRestore()} to
349      * close all open XML Tags
350      *
351      * @throws IOException
352      */
353     public void writeTrailer() throws IOException {
354         // write font definition
355         if (isProperty(EMBED_FONTS)) {
356             os.println("<defs>");
357             os.println(fontTable.toString());
358             os.println("</defs> <!-- font definitions -->");
359         }
360 
361         // restor graphic
362         writeGraphicsRestore();
363     }
364 
365     public void closeStream() throws IOException {
366         os.close();
367     }
368 
369     /*
370      * ================================================================================ |
371      * 4. Create
372      * ================================================================================
373      */
374 
375     public Graphics create() {
376         try {
377             writeGraphicsSave();
378         } catch (IOException e) {
379             handleException(e);
380         }
381         return new SVGGraphics2D(this, true);
382     }
383 
384     public Graphics create(double x, double y, double width, double height) {
385         try {
386             writeGraphicsSave();
387         } catch (IOException e) {
388             handleException(e);
389         }
390         SVGGraphics2D graphics = new SVGGraphics2D(this, true);
391         // FIXME: All other drivers have a translate(x,y), clip(0,0,w,h) here
392         os.println("<svg x=\"" + fixedPrecision(x) + "\" " + "y=\""
393                 + fixedPrecision(y) + "\" " + "width=\""
394                 + fixedPrecision(width) + "\" " + "height=\""
395                 + fixedPrecision(height) + "\" " + ">");
396         graphics.closeTags.push("</svg> <!-- graphics context -->");
397 
398         // write default stroke
399         os.print("<g ");
400         Properties style = getStrokeProperties(defaultStroke,  true);
401         os.print(style(style));
402         os.println(">");
403 
404         graphics.closeTags.push("</g> <!-- default stroke -->");
405 
406         return graphics;
407     }
408 
409     protected void writeGraphicsSave() throws IOException {
410         // not applicable
411     }
412 
413     protected void writeGraphicsRestore() throws IOException {
414         while (!closeTags.empty()) {
415             os.println(closeTags.pop());
416         }
417     }
418 
419     /*
420      * ================================================================================ |
421      * 5. Drawing Methods
422      * ================================================================================
423      */
424     /* 5.1 shapes */
425     /* 5.1.4. shapes */
426 
427     /**
428      * Draws the shape using the current paint as border
429      *
430      * @param shape Shape to draw
431      */
432     public void draw(Shape shape) {
433         // others than BasicStrokes are written by its
434         // {@link Stroke#createStrokedShape()}
435         if (getStroke() instanceof BasicStroke) {
436             PathIterator path = shape.getPathIterator(null);
437 
438             Properties style = new Properties();
439             if (getPaint() != null) {
440                 style.put("stroke", hexColor(getPaint()));
441                 style.put("stroke-opacity", fixedPrecision(alphaColor(getPaint())));
442             }
443 
444             // no filling
445             style.put("fill", "none");
446             style.putAll(getStrokeProperties(getStroke(), false));
447 
448             writePathIterator(path, style);
449         } else if (getStroke() != null) {
450             // fill the shape created by stroke
451             fill(getStroke().createStrokedShape(shape));
452         } else {
453             // FIXME: do nothing or draw using defaultStroke?
454             fill(defaultStroke.createStrokedShape(shape));
455         }
456     }
457 
458     /**
459      * Fills the shape without a border using the current paint
460      *
461      * @param shape Shape to be filled with the current paint
462      */
463     public void fill(Shape shape) {
464         // draw paint as image if needed
465         if (!(getPaint() instanceof Color || getPaint() instanceof GradientPaint)) {
466             // draw paint as image
467             fill(shape, getPaint());
468         } else {
469             PathIterator path = shape.getPathIterator(null);
470 
471             Properties style = new Properties();
472 
473             if (path.getWindingRule() == PathIterator.WIND_EVEN_ODD) {
474                 style.put("fill-rule", "evenodd");
475             } else {
476                 style.put("fill-rule", "nonzero");
477             }
478 
479             // fill with paint
480             if (getPaint() != null) {
481                 style.put("fill", hexColor(getPaint()));
482                 style.put("fill-opacity", fixedPrecision(alphaColor(getPaint())));
483             }
484 
485             // no border
486             style.put("stroke", "none");
487 
488             writePathIterator(path, style);
489         }
490     }
491 
492     /**
493      * writes a path using {@link #getPath(java.awt.geom.PathIterator)}
494      * and the given style
495      *
496      * @param pi PathIterator
497      * @param style Properties for <g> tag
498      */
499     private void writePathIterator(PathIterator pi, Properties style) {
500         StringBuffer result = new StringBuffer();
501 
502         // write style
503         result.append("<g ");
504         result.append(style(style));
505         result.append(">\n  ");
506 
507         // draw shape
508         result.append(getPath(pi));
509 
510         // close style
511         result.append("\n</g> <!-- drawing style -->");
512 
513         boolean drawClipped = false;
514 
515         // test if clip intersects pi
516         if (getClip() != null) {
517             GeneralPath gp = new GeneralPath();
518             gp.append(pi, true);
519             // create the stroked shape
520             Stroke stroke = getStroke() == null? defaultStroke : getStroke();
521             Rectangle2D bounds = stroke.createStrokedShape(gp).getBounds();
522             // clip should intersect the path
523             // if clip contains the bounds completely, clipping is not needed
524             drawClipped = getClip().intersects(bounds) && !getClip().contains(bounds);
525         }
526 
527         if (drawClipped) {
528             // write in a transformed and clipped context
529             os.println(
530                 getTransformedString(
531                     getTransform(),
532                     getClippedString(result.toString())));
533         } else {
534             // write in a transformed context
535             os.println(
536                 getTransformedString(
537                     getTransform(),
538                     result.toString()));
539         }
540     }
541 
542     /* 5.2. Images */
543     public void copyArea(int x, int y, int width, int height, int dx, int dy) {
544         writeWarning(getClass()
545                 + ": copyArea(int, int, int, int, int, int) not implemented.");
546     }
547 
548     protected void writeImage(RenderedImage image, AffineTransform xform,
549             Color bkg) throws IOException {
550 
551         StringBuffer result = new StringBuffer();
552 
553         result.append("<image x=\"0\" y=\"0\" " + "width=\"");
554         result.append(image.getWidth());
555         result.append("\" " + "height=\"");
556         result.append(image.getHeight());
557         result.append("\" " + "xlink:href=\"");
558 
559         String writeAs = getProperty(WRITE_IMAGES_AS);
560         boolean isTransparent = image.getColorModel().hasAlpha()
561                 && (bkg == null);
562 
563         String encode;
564         byte[] imageBytes;
565 
566         // write as PNG
567         if (ImageConstants.PNG.equalsIgnoreCase(writeAs) || isTransparent) {
568             encode = ImageConstants.PNG;
569             imageBytes = ImageGraphics2D.toByteArray(
570                 image, ImageConstants.PNG, null, null);
571         }
572 
573         // write as JPG
574         else if (ImageConstants.JPG.equalsIgnoreCase(writeAs)) {
575             encode = ImageConstants.JPG;
576             imageBytes = ImageGraphics2D.toByteArray(
577                 image, ImageConstants.JPG, null, null);
578         }
579 
580         // write as SMALLEST
581         else {
582             byte[] pngBytes = ImageGraphics2D.toByteArray(image, ImageConstants.PNG, null, null);
583             byte[] jpgBytes = ImageGraphics2D.toByteArray(image, ImageConstants.JPG, null, null);
584 
585             // define encode and imageBytes
586             if (jpgBytes.length < 0.5 * pngBytes.length) {
587                 encode = ImageConstants.JPG;
588                 imageBytes = jpgBytes;
589             } else {
590                 encode = ImageConstants.PNG;
591                 imageBytes = pngBytes;
592             }
593         }
594 
595         if (isProperty(EXPORT_IMAGES)) {
596             imageNumber++;
597 
598             // create filenames
599             if (filename == null) {
600                 writeWarning("SVG: cannot write embedded images, since SVGGraphics2D");
601                 writeWarning("     was created from an OutputStream rather than a File.");
602                 return;
603             }
604             int pos = filename.lastIndexOf(File.separatorChar);
605             String dirName = (pos < 0) ? "" : filename.substring(0, pos + 1);
606             String imageName = (pos < 0) ? filename : filename
607                     .substring(pos + 1);
608             imageName += "." + getProperty(EXPORT_SUFFIX) + "-" + imageNumber
609                     + "." + encode;
610 
611             result.append(imageName);
612 
613             // write the image separately
614             FileOutputStream imageStream = new FileOutputStream(dirName
615                     + imageName);
616 
617             imageStream.write(imageBytes);
618             imageStream.close();
619         } else {
620             result.append("data:image/");
621             result.append(encode);
622             result.append(";base64,");
623 
624             StringWriter writer = new StringWriter();
625             Base64OutputStream b64 = new Base64OutputStream(
626                     new WriterOutputStream(writer));
627             b64.write(imageBytes);
628             b64.finish();
629 
630             result.append(writer.toString());
631         }
632 
633         result.append("\"/>");
634 
635         os.println(getTransformedString(getTransform(),
636             getClippedString(getTransformedString(xform, result
637                 .toString()))));
638     }
639 
640     /* 5.3. Strings */
641     protected void writeString(String str, double x, double y)
642             throws IOException {
643         // str = FontEncoder.getEncodedString(str, getFont().getName());
644 
645         if (isProperty(EMBED_FONTS)) {
646             fontTable.addGlyphs(str, getFont());
647         }
648 
649         // font transformation should _not_ transform string position
650         // so we draw at 0:0 and translate _before_ using getFont().getTransform()
651         // we could not just translate before and reverse translation after
652         // writing because the clipping area
653 
654         // create font properties
655         Properties style = getFontProperties(getFont());
656 
657         // add stroke properties
658         if (getPaint() != null) {
659             style.put("fill", hexColor(getPaint()));
660             style.put("fill-opacity", fixedPrecision(alphaColor(getPaint())));
661         } else {
662             style.put("fill", "none");
663         }
664         style.put("stroke", "none");
665 
666         // convert tags to string values
667         str = XMLWriter.normalizeText(str);
668 
669         // replace leading space by &#00a0; otherwise firefox 1.5 fails
670         if (str.startsWith(" ")) {
671             str = "&#x00a0;" + str.substring(1);
672         }
673 
674         os.println(getTransformedString(
675             // general transformation
676             getTransform(),
677             // general clip
678             getClippedString(
679                 getTransformedString(
680                     // text offset
681                     new AffineTransform(1, 0, 0, 1, x, y),
682                     getTransformedString(
683                         // font transformation and text
684                         getFont().getTransform(),
685                         "<text "
686                             // style
687                             + style(style)
688                             // coordiantes
689                             + " x=\"0\" y=\"0\">"
690                             // text
691                             + str
692                             + "</text>")))));
693     }
694 
695     /**
696      * Creates the properties list for the given font.
697      * Family, size, bold italic, underline and strikethrough are converted.
698      * {@link java.awt.font.TextAttribute#SUPERSCRIPT}
699      * is handled by {@link java.awt.Font#getTransform()}
700      *
701      * @return properties in svg style  for the font
702      * @param font Font to
703      */
704     private Properties getFontProperties(Font font) {
705         Properties result = new Properties();
706 
707         // attribute for font properties
708         Map /*<TextAttribute, ?>*/ attributes = FontUtilities.getAttributes(font);
709 
710         // dialog.bold -> Helvetica with TextAttribute.WEIGHT_BOLD
711         SVGFontTable.normalize(attributes);
712 
713         // family
714         result.put("font-family", attributes.get(TextAttribute.FAMILY));
715 
716         // weight
717         if (TextAttribute.WEIGHT_BOLD.equals(attributes.get(TextAttribute.WEIGHT))) {
718             result.put("font-weight", "bold");
719         } else {
720             result.put("font-weight", "normal");
721         }
722 
723         // posture
724         if (TextAttribute.POSTURE_OBLIQUE.equals(attributes.get(TextAttribute.POSTURE))) {
725             result.put("font-style", "italic");
726         } else {
727             result.put("font-style", "normal");
728         }
729 
730         Object ul = attributes.get(TextAttribute.UNDERLINE);
731         if (ul != null) {
732             // underline style, only supported by CSS 3
733             if (TextAttribute.UNDERLINE_LOW_DOTTED.equals(ul)) {
734                 result.put("text-underline-style", "dotted");
735             } else if (TextAttribute.UNDERLINE_LOW_DASHED.equals(ul)) {
736                 result.put("text-underline-style", "dashed");
737             } else if (TextAttribute.UNDERLINE_ON.equals(ul)) {
738                 result.put("text-underline-style", "solid");
739             }
740 
741             // the underline itself, supported by CSS 2
742             result.put("text-decoration", "underline");
743         }
744 
745         if (attributes.get(TextAttribute.STRIKETHROUGH) != null) {
746             // is the property allready witten?
747             if  (ul == null) {
748                 result.put("text-decoration", "underline, line-through");
749             } else {
750                 result.put("text-decoration", "line-through");
751             }
752         }
753 
754         Float size = (Float) attributes.get(TextAttribute.SIZE);
755         result.put("font-size", fixedPrecision(size.floatValue()));
756 
757         return result;
758     }
759 
760     /*
761      * ================================================================================ |
762      * 6. Transformations
763      * ================================================================================
764      */
765     protected void writeTransform(AffineTransform transform) throws IOException {
766         // written when needed
767     }
768 
769     protected void writeSetTransform(AffineTransform transform)
770             throws IOException {
771         // written when needed
772     }
773 
774     /*
775      * ================================================================================ |
776      * 7. Clipping
777      * ================================================================================
778      */
779     protected void writeClip(Shape s) throws IOException {
780         // written when needed
781     }
782 
783     protected void writeSetClip(Shape s) throws IOException {
784         // written when needed
785     }
786 
787     /*
788      * ================================================================================ |
789      * 8. Graphics State
790      * ================================================================================
791      */
792     /* 8.1. stroke/linewidth */
793     protected void writeWidth(float width) throws IOException {
794         // written when needed
795     }
796 
797     protected void writeCap(int cap) throws IOException {
798         // Written when needed
799     }
800 
801     protected void writeJoin(int join) throws IOException {
802         // written when needed
803     }
804 
805     protected void writeMiterLimit(float limit) throws IOException {
806         // written when needed
807     }
808 
809     protected void writeDash(float[] dash, float phase) throws IOException {
810         // written when needed
811     }
812 
813     /**
814      * return the style tag for the stroke
815      *
816      * @param s
817      *            Stroke to convert
818      * @param all
819      *            all attributes (not only the differences to defaultStroke) are
820      *            handled
821      * @return corresponding style string
822      */
823     private Properties getStrokeProperties(Stroke s, boolean all) {
824         Properties result = new Properties();
825 
826         // only BasisStrokes are written
827         if (!(s instanceof BasicStroke)) {
828             return result;
829         }
830 
831         BasicStroke stroke = (BasicStroke) s;
832 
833         // append linecap
834         if (all || (stroke.getEndCap() != defaultStroke.getEndCap())) {
835             // append cap
836             switch (stroke.getEndCap()) {
837                 default:
838                 case BasicStroke.CAP_BUTT:
839                     result.put("stroke-linecap", "butt");
840                     break;
841                 case BasicStroke.CAP_ROUND:
842                     result.put("stroke-linecap", "round");
843                     break;
844                 case BasicStroke.CAP_SQUARE:
845                     result.put("stroke-linecap", "square");
846                     break;
847             }
848         }
849 
850         // append dasharray
851         if (all
852                 || !Arrays.equals(stroke.getDashArray(), defaultStroke
853                         .getDashArray())) {
854             if (stroke.getDashArray() != null
855                     && stroke.getDashArray().length > 0) {
856                 StringBuffer array = new StringBuffer();
857                 for (int i = 0; i < stroke.getDashArray().length; i++) {
858                     if (i > 0) {
859                         array.append(",");
860                     }
861                     // SVG does not allow dash entry to be zero (Firefox 2.0).
862                     float dash = stroke.getDashArray()[i];
863                     array.append(fixedPrecision(dash > 0 ? dash : 0.1));
864                 }
865                 result.put("stroke-dasharray", array.toString());
866             } else {
867                 result.put("stroke-dasharray", "none");
868             }
869         }
870 
871         if (all || (stroke.getDashPhase() != defaultStroke.getDashPhase())) {
872             result.put("stroke-dashoffset", fixedPrecision(stroke.getDashPhase()));
873         }
874 
875         // append meter limit
876         if (all || (stroke.getMiterLimit() != defaultStroke.getMiterLimit())) {
877             result.put("stroke-miterlimit", fixedPrecision(stroke.getMiterLimit()));
878         }
879 
880         // append join
881         if (all || (stroke.getLineJoin() != defaultStroke.getLineJoin())) {
882             switch (stroke.getLineJoin()) {
883                 default:
884                 case BasicStroke.JOIN_MITER:
885                     result.put("stroke-linejoin", "miter");
886                     break;
887                 case BasicStroke.JOIN_ROUND:
888                     result.put("stroke-linejoin", "round");
889                     break;
890                 case BasicStroke.JOIN_BEVEL:
891                     result.put("stroke-linejoin", "bevel");
892                     break;
893             }
894         }
895 
896         // append linewidth
897         if (all || (stroke.getLineWidth() != defaultStroke.getLineWidth())) {
898             // width of 0 means thinnest line, which does not exist in SVG
899             if (stroke.getLineWidth() == 0) {
900                 result.put("stroke-width", fixedPrecision(0.000001f));
901             } else {
902                 result.put("stroke-width", fixedPrecision(stroke.getLineWidth()));
903             }
904         }
905 
906         return result;
907     }
908 
909     /* 8.2. paint/color */
910     public void setPaintMode() {
911         writeWarning(getClass() + ": setPaintMode() not implemented.");
912     }
913 
914     public void setXORMode(Color c1) {
915         writeWarning(getClass() + ": setXORMode(Color) not implemented.");
916     }
917 
918     protected void writePaint(Color c) throws IOException {
919         // written with every draw
920     }
921 
922     protected void writePaint(GradientPaint paint) throws IOException {
923         if (gradients.get(paint) == null) {
924             String name = "gradient-" + gradients.size();
925             gradients.put(paint, name);
926             Point2D p1 = paint.getPoint1();
927             Point2D p2 = paint.getPoint2();
928             os.println("<defs>");
929             os.print("  <linearGradient id=\"" + name + "\" ");
930             os.print("x1=\"" + fixedPrecision(p1.getX()) + "\" ");
931             os.print("y1=\"" + fixedPrecision(p1.getY()) + "\" ");
932             os.print("x2=\"" + fixedPrecision(p2.getX()) + "\" ");
933             os.print("y2=\"" + fixedPrecision(p2.getY()) + "\" ");
934             os.print("gradientUnits=\"userSpaceOnUse\" ");
935             os.print("spreadMethod=\""
936                     + ((paint.isCyclic()) ? "reflect" : "pad") + "\" ");
937             os.println(">");
938             os.println("    <stop offset=\"0\" stop-color=\""
939                     + hexColor(paint.getColor1()) + "\" " + "opacity-stop=\""
940                     + alphaColor(paint.getColor1()) + "\" />");
941             os.println("    <stop offset=\"1\" stop-color=\""
942                     + hexColor(paint.getColor2()) + "\" " + "opacity-stop=\""
943                     + alphaColor(paint.getColor2()) + "\" />");
944             os.println("  </linearGradient>");
945             os.println("</defs>");
946         }
947 
948         // create style
949         Properties style = new Properties();
950         style.put("stroke", hexColor(getPaint()));
951 
952         // write style
953         os.print("<g ");
954         os.print(style(style));
955         os.println(">");
956 
957         // close color later
958         closeTags.push("</g> <!-- color -->");
959     }
960 
961     protected void writePaint(TexturePaint paint) throws IOException {
962         // written when needed
963     }
964 
965     protected void writePaint(Paint p) throws IOException {
966         // written when needed
967     }
968 
969     /* 8.3. font */
970     protected void writeFont(Font font) throws IOException {
971         // written when needed
972     }
973 
974     /*
975      * ================================================================================ |
976      * 9. Auxiliary
977      * ================================================================================
978      */
979     public GraphicsConfiguration getDeviceConfiguration() {
980         writeWarning(getClass() + ": getDeviceConfiguration() not implemented.");
981         return null;
982     }
983 
984     public void writeComment(String s) throws IOException {
985         os.println("<!-- " + s + " -->");
986     }
987 
988     public String toString() {
989         return "SVGGraphics2D";
990     }
991 
992     /*
993      * ================================================================================ |
994      * 10. Private/Utility Methos
995      * ================================================================================
996      */
997 
998     /**
999      * Encapsulates a SVG-Tag by the given transformation matrix
1000      *
1001      * @param t
1002      *            Transformation
1003      * @param s
1004      *            SVG-Tag
1005      */
1006     private String getTransformedString(AffineTransform t, String s) {
1007         StringBuffer result = new StringBuffer();
1008 
1009         if (t != null && !t.isIdentity()) {
1010             result.append("<g transform=\"matrix(");
1011             result.append(fixedPrecision(t.getScaleX()));
1012             result.append(", ");
1013             result.append(fixedPrecision(t.getShearY()));
1014             result.append(", ");
1015             result.append(fixedPrecision(t.getShearX()));
1016             result.append(", ");
1017             result.append(fixedPrecision(t.getScaleY()));
1018             result.append(", ");
1019             result.append(fixedPrecision(t.getTranslateX()));
1020             result.append(", ");
1021             result.append(fixedPrecision(t.getTranslateY()));
1022             result.append(")\">\n");
1023         }
1024 
1025         result.append(s);
1026 
1027         if (t != null && !t.isIdentity()) {
1028             result.append("\n</g> <!-- transform -->");
1029         }
1030 
1031         return result.toString();
1032     }
1033 
1034     /**
1035      * Encapsulates a SVG-Tag by the current clipping area matrix
1036      *
1037      * @param s SVG-Tag
1038      * @return SVG Tag encapsulated by the current clip
1039      */
1040     private String getClippedString(String s) {
1041         StringBuffer result = new StringBuffer();
1042 
1043         // clipping
1044         if (isProperty(CLIP) && getClip() != null) {
1045             // SVG uses unique lip numbers, don't reset allways increment them
1046             clipNumber.set(clipNumber.getInt() + 1);
1047 
1048             // define clip
1049             result.append("<clipPath id=\"clip");
1050             result.append(clipNumber.getInt());
1051             result.append("\">\n  ");
1052             result.append(getPath(getClip().getPathIterator(null)));
1053             result.append("\n</clipPath>\n");
1054 
1055             // use clip
1056             result.append("<g clip-path=\"url(#clip");
1057             result.append(clipNumber.getInt());
1058             result.append(")\">\n");
1059         }
1060 
1061         // append the string
1062         result.append(s);
1063 
1064         // close clipping
1065         if (isProperty(CLIP) && getClip() != null) {
1066             result.append("\n</g> <!-- clip");
1067             result.append(clipNumber.getInt());
1068             result.append(" -->");
1069         }
1070 
1071         return result.toString();
1072     }
1073 
1074     private float alphaColor(Paint p) {
1075         if (p instanceof Color) {
1076             return (float) (getPrintColor((Color) p).getAlpha() / 255.0);
1077         } else if (p instanceof GradientPaint) {
1078             return 1.0f;
1079         } else if (p instanceof TexturePaint) {
1080             return 1.0f;
1081         }
1082         writeWarning(getClass() + ": alphaColor() not implemented for "
1083                 + p.getClass() + ".");
1084         return 1.0f;
1085     }
1086 
1087     private String hexColor(Paint p) {
1088         if (p instanceof Color) {
1089             return hexColor(getPrintColor((Color) p));
1090         } else if (p instanceof GradientPaint) {
1091             return hexColor((GradientPaint) p);
1092         } else if (p instanceof TexturePaint) {
1093             return hexColor((TexturePaint) p);
1094         }
1095         writeWarning(getClass() + ": hexColor() not implemented for "
1096                 + p.getClass() + ".");
1097         return "#000000";
1098     }
1099 
1100     private String hexColor(Color c) {
1101         String s1 = Integer.toHexString(c.getRed());
1102         s1 = (s1.length() != 2) ? "0" + s1 : s1;
1103 
1104         String s2 = Integer.toHexString(c.getGreen());
1105         s2 = (s2.length() != 2) ? "0" + s2 : s2;
1106 
1107         String s3 = Integer.toHexString(c.getBlue());
1108         s3 = (s3.length() != 2) ? "0" + s3 : s3;
1109 
1110         return "#" + s1 + s2 + s3;
1111     }
1112 
1113     private String hexColor(GradientPaint p) {
1114         return "url(#" + gradients.get(p) + ")";
1115     }
1116 
1117     private String hexColor(TexturePaint p) {
1118         return "url(#" + textures.get(p) + ")";
1119     }
1120 
1121     protected static String getPathContent(PathIterator path) {
1122         StringBuffer result = new StringBuffer();
1123 
1124         double[] coords = new double[6];
1125         result.append("d=\"");
1126         while (!path.isDone()) {
1127             int segType = path.currentSegment(coords);
1128 
1129             switch (segType) {
1130                 case PathIterator.SEG_MOVETO:
1131                     result.append("M ");
1132                     result.append(fixedPrecision(coords[0]));
1133                     result.append(" ");
1134                     result.append(fixedPrecision(coords[1]));
1135                     break;
1136                 case PathIterator.SEG_LINETO:
1137                     result.append("L ");
1138                     result.append(fixedPrecision(coords[0]));
1139                     result.append(" ");
1140                     result.append(fixedPrecision(coords[1]));
1141                     break;
1142                 case PathIterator.SEG_CUBICTO:
1143                     result.append("C ");
1144                     result.append(fixedPrecision(coords[0]));
1145                     result.append(" ");
1146                     result.append(fixedPrecision(coords[1]));
1147                     result.append(" ");
1148                     result.append(fixedPrecision(coords[2]));
1149                     result.append(" ");
1150                     result.append(fixedPrecision(coords[3]));
1151                     result.append(" ");
1152                     result.append(fixedPrecision(coords[4]));
1153                     result.append(" ");
1154                     result.append(fixedPrecision(coords[5]));
1155                     break;
1156                 case PathIterator.SEG_QUADTO:
1157                     result.append("Q ");
1158                     result.append(fixedPrecision(coords[0]));
1159                     result.append(" ");
1160                     result.append(fixedPrecision(coords[1]));
1161                     result.append(" ");
1162                     result.append(fixedPrecision(coords[2]));
1163                     result.append(" ");
1164                     result.append(fixedPrecision(coords[3]));
1165                     break;
1166                 case PathIterator.SEG_CLOSE:
1167                     result.append("z");
1168                     break;
1169             }
1170 
1171             // Move to the next segment.
1172             path.next();
1173 
1174             // Not needed but makes the output readable
1175             if (!path.isDone()) {
1176                 result.append(" ");
1177             }
1178         }
1179         result.append("\"");
1180 
1181         return result.toString();
1182     }
1183 
1184     protected String getPath(PathIterator path) {
1185         StringBuffer result = new StringBuffer();
1186 
1187         result.append("<path ");
1188         result.append(getPathContent(path));
1189         result.append("/>");
1190 
1191         return result.toString();
1192     }
1193 
1194     /**
1195      * For a given "key -> value" property set the
1196      * method creates
1197      * style="key1:value1;key2:value2;" or
1198      * key2="value2" key2="value2" depending on
1199      * {@link #STYLABLE}.
1200      *
1201      * @param style properties to convert
1202      * @return String
1203      */
1204     private String style(Properties style) {
1205         if (style == null || style.isEmpty()) {
1206             return "";
1207         }
1208 
1209         StringBuffer result = new StringBuffer();
1210         boolean styleable = isProperty(STYLABLE);
1211 
1212         // embed everything in a "style" attribute
1213         if (styleable) {
1214             result.append("style=\"");
1215         }
1216 
1217         Enumeration keys = style.keys();
1218         while (keys.hasMoreElements()) {
1219             String key = (String) keys.nextElement();
1220             String value = style.getProperty(key);
1221 
1222             result.append(key);
1223 
1224             if (styleable) {
1225                 result.append(":");
1226                 result.append(value);
1227                 result.append(";");
1228             } else {
1229                 result.append("=\"");
1230                 result.append(value);
1231                 result.append("\"");
1232                 if (keys.hasMoreElements()) {
1233                     result.append(" ");
1234                 }
1235             }
1236         }
1237 
1238         // close the style attribute
1239         if (styleable) {
1240             result.append("\"");
1241         }
1242 
1243         return result.toString();
1244     }
1245 
1246     /**
1247      * for fixedPrecision(double d), SVG does not understand "1E-7"
1248      * we have to use ".0000007" instead
1249      */
1250     private static DecimalFormat scientific = new DecimalFormat(
1251         "#.####################",
1252         new DecimalFormatSymbols(Locale.US));
1253 
1254     /**
1255      * converts the double value to a representing string
1256      *
1257      * @param d double value to convert
1258      * @return same as string
1259      */
1260     public static String fixedPrecision(double d) {
1261         return scientific.format(d);
1262     }
1263 
1264     protected PrintWriter getOutputStream() {
1265         return os;
1266     }
1267 }