001/* ===================================================
002 * JFreeSVG : an SVG library for the Java(tm) platform
003 * ===================================================
004 * 
005 * (C)opyright 2013, 2014, by Object Refinery Limited.  All rights reserved.
006 *
007 * Project Info:  http://www.jfree.org/jfreesvg/index.html
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * JFreeSVG home page:
028 * 
029 * http://www.jfree.org/jfreesvg
030 */
031
032package org.jfree.graphics2d.svg;
033
034import java.awt.AlphaComposite;
035import java.awt.BasicStroke;
036import java.awt.Color;
037import java.awt.Composite;
038import java.awt.Font;
039import java.awt.FontMetrics;
040import java.awt.GradientPaint;
041import java.awt.Graphics;
042import java.awt.Graphics2D;
043import java.awt.GraphicsConfiguration;
044import java.awt.Image;
045import java.awt.LinearGradientPaint;
046import java.awt.MultipleGradientPaint.CycleMethod;
047import java.awt.Paint;
048import java.awt.RadialGradientPaint;
049import java.awt.Rectangle;
050import java.awt.RenderingHints;
051import java.awt.Shape;
052import java.awt.Stroke;
053import java.awt.font.FontRenderContext;
054import java.awt.font.GlyphVector;
055import java.awt.font.TextLayout;
056import java.awt.geom.AffineTransform;
057import java.awt.geom.Arc2D;
058import java.awt.geom.Area;
059import java.awt.geom.Ellipse2D;
060import java.awt.geom.GeneralPath;
061import java.awt.geom.Line2D;
062import java.awt.geom.NoninvertibleTransformException;
063import java.awt.geom.Path2D;
064import java.awt.geom.PathIterator;
065import java.awt.geom.Point2D;
066import java.awt.geom.Rectangle2D;
067import java.awt.geom.RoundRectangle2D;
068import java.awt.image.BufferedImage;
069import java.awt.image.BufferedImageOp;
070import java.awt.image.ImageObserver;
071import java.awt.image.RenderedImage;
072import java.awt.image.renderable.RenderableImage;
073import java.io.ByteArrayOutputStream;
074import java.io.IOException;
075import java.text.AttributedCharacterIterator;
076import java.text.AttributedCharacterIterator.Attribute;
077import java.text.AttributedString;
078import java.text.DecimalFormat;
079import java.text.DecimalFormatSymbols;
080import java.util.ArrayList;
081import java.util.HashMap;
082import java.util.HashSet;
083import java.util.List;
084import java.util.Map;
085import java.util.Map.Entry;
086import java.util.Set;
087import java.util.logging.Level;
088import java.util.logging.Logger;
089import javax.imageio.ImageIO;
090import javax.xml.bind.DatatypeConverter;
091import org.jfree.graphics2d.Args;
092import org.jfree.graphics2d.GradientPaintKey;
093import org.jfree.graphics2d.GraphicsUtils;
094import org.jfree.graphics2d.LinearGradientPaintKey;
095import org.jfree.graphics2d.RadialGradientPaintKey;
096
097/**
098 * A {@code Graphics2D} implementation that creates SVG output.  After 
099 * rendering the graphics via the {@code SVGGraphics2D}, you can retrieve
100 * an SVG element (see {@link #getSVGElement()}) or an SVG document (see 
101 * {@link #getSVGDocument()}) containing your content.
102 * <p>
103 * <b>Usage</b><br>
104 * Using the {@code SVGGraphics2D} class is straightforward.  First, 
105 * create an instance specifying the height and width of the SVG element that 
106 * will be created.  Then, use standard Java2D API calls to draw content 
107 * into the element.  Finally, retrieve the SVG element that has been 
108 * accumulated.  For example:
109 * <p>
110 * {@code &nbsp;&nbsp;&nbsp;&nbsp;SVGGraphics2D g2 = new SVGGraphics2D(300, 200);<br>}
111 * {@code &nbsp;&nbsp;&nbsp;&nbsp;g2.setPaint(Color.RED);<br>}
112 * {@code &nbsp;&nbsp;&nbsp;&nbsp;g2.draw(new Rectangle(10, 10, 280, 180);<br>}
113 * {@code &nbsp;&nbsp;&nbsp;&nbsp;String svgElement = g2.getSVGElement();<br>}
114 * <p>
115 * For the content generation step, you can make use of third party libraries,
116 * such as <a href="http://www.jfree.org/jfreechart/">JFreeChart</a> and
117 * <a href="http://www.object-refinery.com/orsoncharts/">Orson Charts</a>, that 
118 * render output using standard Java2D API calls.
119 * <p>
120 * <b>Rendering Hints</b><br>
121 * The {@code SVGGraphics2D} supports a couple of custom rendering hints -  
122 * for details, refer to the {@link SVGHints} class documentation.  Also see
123 * the examples in this blog post: 
124 * <a href="http://www.object-refinery.com/blog/blog-20140509.html">
125 * Orson Charts 3D / Enhanced SVG Export</a>.
126 * <p>
127 * <b>Other Notes</b><br>
128 * Some additional notes:
129 * <ul>
130 * <li>Images are supported, but for methods with an {@code ImageObserver}
131 * parameter note that the observer is ignored completely.  In any case, using 
132 * images that are not fully loaded already would not be a good idea in the 
133 * context of generating SVG data/files;</li>
134 * 
135 * <li>the {@link #getFontMetrics(java.awt.Font)} and 
136 * {@link #getFontRenderContext()} methods return values that come from an 
137 * internal {@code BufferedImage}, this is a short-cut and we don't know 
138 * if there are any negative consequences (if you know of any, please let us 
139 * know and we'll add the info here or find a way to fix it);</li>
140 * 
141 * <li>there are settings to control the number of decimal places used to
142 * write the coordinates for geometrical elements (default 2dp) and transform
143 * matrices (default 6dp).  These defaults may change in a future release.</li>
144 * 
145 * <li>when an HTML page contains multiple SVG elements, the items within
146 * the DEFS element for each SVG element must have IDs that are unique across 
147 * <em>all</em> SVG elements in the page.  We auto-populate the 
148 * {@code defsKeyPrefix} attribute to help ensure that unique IDs are 
149 * generated.</li>
150 * </ul>
151 *
152 * <p>For some demos showing how to use this class, look in the
153 * {@code org.jfree.graphics2d.demo} package in the{@code src} directory.</p>
154 */
155public final class SVGGraphics2D extends Graphics2D {
156
157    /** The prefix for keys used to identify clip paths. */
158    private static final String CLIP_KEY_PREFIX = "clip-";
159    
160    private final int width;
161    
162    private final int height;
163    
164    /** 
165     * The shape rendering property to set for the SVG element.  Permitted
166     * values are "auto", "crispEdges", "geometricPrecision" and
167     * "optimizeSpeed".
168     */
169    private String shapeRendering = "auto";
170    
171    /**
172     * The text rendering property for the SVG element.  Permitted values 
173     * are "auto", "optimizeSpeed", "optimizeLegibility" and 
174     * "geometricPrecision".
175     */
176    private String textRendering = "auto";
177    
178    /** Rendering hints (see SVGHints). */
179    private final RenderingHints hints;
180    
181    /** 
182     * A flag that controls whether or not the KEY_STROKE_CONTROL hint is
183     * checked.
184     */
185    private boolean checkStrokeControlHint = true;
186    
187    /** 
188     * The number of decimal places to use when writing the matrix values
189     * for transformations. 
190     */
191    private int transformDP;
192    
193    /**
194     * The decimal formatter for transform matrices.
195     */
196    private DecimalFormat transformFormat;
197    
198    /**
199     * The number of decimal places to use when writing coordinates for
200     * geometrical shapes.
201     */
202    private int geometryDP;
203
204    /**
205     * The decimal formatter for coordinates of geometrical shapes.
206     */
207    private DecimalFormat geometryFormat;
208    
209    /** The buffer that accumulates the SVG output. */
210    private final StringBuilder sb;
211
212    /** 
213     * A prefix for the keys used in the DEFS element.  This can be used to 
214     * ensure that the keys are unique when creating more than one SVG element
215     * for a single HTML page.
216     */
217    private String defsKeyPrefix = "";
218    
219    /** 
220     * A map of all the gradients used, and the corresponding id.  When 
221     * generating the SVG file, all the gradient paints used must be defined
222     * in the defs element.
223     */
224    private final Map<GradientPaintKey, String> gradientPaints 
225            = new HashMap<GradientPaintKey, String>();
226    
227    /** 
228     * A map of all the linear gradients used, and the corresponding id.  When 
229     * generating the SVG file, all the linear gradient paints used must be 
230     * defined in the defs element.
231     */
232    private final Map<LinearGradientPaintKey, String> linearGradientPaints 
233            = new HashMap<LinearGradientPaintKey, String>();
234    
235    /** 
236     * A map of all the radial gradients used, and the corresponding id.  When 
237     * generating the SVG file, all the radial gradient paints used must be 
238     * defined in the defs element.
239     */
240    private final Map<RadialGradientPaintKey, String> radialGradientPaints
241            = new HashMap<RadialGradientPaintKey, String>();
242    
243    /**
244     * A list of the registered clip regions.  These will be written to the
245     * DEFS element.
246     */
247    private final List<String> clipPaths = new ArrayList<String>();
248    
249    /** 
250     * The filename prefix for images that are referenced rather than
251     * embedded but don't have an {@code href} supplied via the 
252     * {@link #KEY_IMAGE_HREF} hint.
253     */
254    private String filePrefix;
255    
256    /** 
257     * The filename suffix for images that are referenced rather than
258     * embedded but don't have an {@code href} supplied via the 
259     * {@link #KEY_IMAGE_HREF} hint.
260     */
261    private String fileSuffix;
262    
263    /** 
264     * A list of images that are referenced but not embedded in the SVG.
265     * After the SVG is generated, the caller can make use of this list to
266     * write PNG files if they don't already exist.  
267     */
268    private final List<ImageElement> imageElements;
269    
270    /** The user clip (can be null). */
271    private Shape clip;
272    
273    /** The reference for the current clip. */
274    private String clipRef;
275    
276    /** The current transform. */
277    private AffineTransform transform = new AffineTransform();
278
279    private Paint paint = Color.BLACK;
280    
281    private Color color = Color.BLACK;
282    
283    private Composite composite = AlphaComposite.getInstance(
284            AlphaComposite.SRC_OVER, 1.0f);
285    
286    /** The current stroke. */
287    private Stroke stroke = new BasicStroke(1.0f);
288    
289    /** 
290     * The width of the SVG stroke to use when the user supplies a
291     * BasicStroke with a width of 0.0 (in this case the Java specification
292     * says "If width is set to 0.0f, the stroke is rendered as the thinnest 
293     * possible line for the target device and the antialias hint setting.")
294     */
295    private double zeroStrokeWidth;
296    
297    /** The last font that was set. */
298    private Font font;
299
300    /** 
301     * The font render context.  The fractional metrics flag solves the glyph
302     * positioning issue identified by Christoph Nahr:
303     * http://news.kynosarges.org/2014/06/28/glyph-positioning-in-jfreesvg-orsonpdf/
304     */
305    private final FontRenderContext fontRenderContext = new FontRenderContext(
306            null, false, true);
307
308    /** Maps font family names to alternates (or leaves them unchanged). */
309    private FontMapper fontMapper;
310        
311    /** The background color, used by clearRect(). */
312    private Color background = Color.BLACK;
313
314    /** A hidden image used for font metrics. */
315    private final BufferedImage fmImage = new BufferedImage(10, 10, 
316            BufferedImage.TYPE_INT_RGB);
317
318    /**
319     * An instance that is lazily instantiated in drawLine and then 
320     * subsequently reused to avoid creating a lot of garbage.
321     */
322    private Line2D line;
323
324    /**
325     * An instance that is lazily instantiated in fillRect and then 
326     * subsequently reused to avoid creating a lot of garbage.
327     */
328    Rectangle2D rect;
329
330    /**
331     * An instance that is lazily instantiated in draw/fillRoundRect and then
332     * subsequently reused to avoid creating a lot of garbage.
333     */
334    private RoundRectangle2D roundRect;
335    
336    /**
337     * An instance that is lazily instantiated in draw/fillOval and then
338     * subsequently reused to avoid creating a lot of garbage.
339     */
340    private Ellipse2D oval;
341 
342    /**
343     * An instance that is lazily instantiated in draw/fillArc and then
344     * subsequently reused to avoid creating a lot of garbage.
345     */
346    private Arc2D arc;
347 
348    /** 
349     * If the current paint is an instance of {@link GradientPaint}, this
350     * field will contain the reference id that is used in the DEFS element
351     * for that linear gradient.
352     */
353    private String gradientPaintRef = null;
354
355    /** 
356     * The device configuration (this is lazily instantiated in the 
357     * getDeviceConfiguration() method.
358     */
359    private GraphicsConfiguration deviceConfiguration;
360
361    /** A set of element IDs. */
362    private final Set<String> elementIDs;
363    
364    /**
365     * Creates a new instance with the specified width and height.
366     * 
367     * @param width  the width of the SVG element.
368     * @param height  the height of the SVG element.
369     */
370    public SVGGraphics2D(int width, int height) {
371        this(width, height, new StringBuilder());
372    }
373    
374    /**
375     * Creates a new instance with the specified width and height that will
376     * populate the supplied StringBuilder instance.  This constructor is 
377     * used by the {@link #create()} method, but won't normally be called
378     * directly by user code.
379     * 
380     * @param width  the width of the SVG element.
381     * @param height  the height of the SVG element.
382     * @param sb  the string builder ({@code null} not permitted).
383     * 
384     * @since 2.0
385     */
386    public SVGGraphics2D(int width, int height, StringBuilder sb) {
387        this.width = width;
388        this.height = height;
389        this.shapeRendering = "auto";
390        this.textRendering = "auto";
391        this.defsKeyPrefix = String.valueOf(System.nanoTime());
392        this.clip = null;
393        this.imageElements = new ArrayList<ImageElement>();
394        this.filePrefix = "image-";
395        this.fileSuffix = ".png";
396        this.font = new Font("SansSerif", Font.PLAIN, 12);
397        this.fontMapper = new StandardFontMapper();
398        this.zeroStrokeWidth = 0.1;
399        this.sb = sb;
400        this.hints = new RenderingHints(SVGHints.KEY_IMAGE_HANDLING, 
401                SVGHints.VALUE_IMAGE_HANDLING_EMBED);
402        // force the formatters to use a '.' for the decimal point
403        DecimalFormatSymbols dfs = new DecimalFormatSymbols();
404        dfs.setDecimalSeparator('.');
405        this.transformFormat = new DecimalFormat("0.######", dfs);
406        this.geometryFormat = new DecimalFormat("0.##", dfs);
407        this.elementIDs = new HashSet<String>();
408    }
409
410    /**
411     * Returns the width for the SVG element, specified in the constructor.
412     * This value will be written to the SVG element returned by the 
413     * {@link #getSVGElement()} method.
414     * 
415     * @return The width for the SVG element. 
416     */
417    public int getWidth() {
418        return this.width;
419    }
420    
421    /**
422     * Returns the height for the SVG element, specified in the constructor.
423     * This value will be written to the SVG element returned by the 
424     * {@link #getSVGElement()} method.
425     * 
426     * @return The height for the SVG element. 
427     */
428    public int getHeight() {
429        return this.height;
430    }
431    
432    /**
433     * Returns the value of the 'shape-rendering' property that will be 
434     * written to the SVG element.  The default value is "auto".
435     * 
436     * @return The shape rendering property.
437     * 
438     * @since 2.0
439     */
440    public String getShapeRendering() {
441        return this.shapeRendering;
442    }
443    
444    /**
445     * Sets the value of the 'shape-rendering' property that will be written to
446     * the SVG element.  Permitted values are "auto", "crispEdges", 
447     * "geometricPrecision", "inherit" and "optimizeSpeed".
448     * 
449     * @param value  the new value.
450     * 
451     * @since 2.0
452     */
453    public void setShapeRendering(String value) {
454        if (!value.equals("auto") && !value.equals("crispEdges") 
455                && !value.equals("geometricPrecision") 
456                && !value.equals("optimizeSpeed")) {
457            throw new IllegalArgumentException("Unrecognised value: " + value);
458        }
459        this.shapeRendering = value;
460    }
461    
462    /**
463     * Returns the value of the 'text-rendering' property that will be 
464     * written to the SVG element.  The default value is "auto".
465     * 
466     * @return The text rendering property.
467     * 
468     * @since 2.0
469     */
470    public String getTextRendering() {
471        return this.textRendering;
472    }
473    
474    /**
475     * Sets the value of the 'text-rendering' property that will be written to
476     * the SVG element.  Permitted values are "auto", "optimizeSpeed", 
477     * "optimizeLegibility" and "geometricPrecision".
478     * 
479     * @param value  the new value.
480     * 
481     * @since 2.0
482     */
483    public void setTextRendering(String value) {
484        if (!value.equals("auto") && !value.equals("optimizeSpeed") 
485                && !value.equals("optimizeLegibility") 
486                && !value.equals("geometricPrecision")) {
487            throw new IllegalArgumentException("Unrecognised value: " + value);
488        }
489        this.textRendering = value;
490    }
491    
492    /**
493     * Returns the flag that controls whether or not this object will observe
494     * the {@code KEY_STROKE_CONTROL} rendering hint.  The default value is
495     * {@code true}.
496     * 
497     * @return A boolean.
498     * 
499     * @see #setCheckStrokeControlHint(boolean) 
500     * @since 2.0
501     */
502    public boolean getCheckStrokeControlHint() {
503        return this.checkStrokeControlHint;
504    }
505    
506    /**
507     * Sets the flag that controls whether or not this object will observe
508     * the {@code KEY_STROKE_CONTROL} rendering hint.  When enabled (the 
509     * default), a hint to normalise strokes will write a {@code stroke-style}
510     * attribute with the value {@code crispEdges}. 
511     * 
512     * @param check  the new flag value.
513     * 
514     * @see #getCheckStrokeControlHint() 
515     * @since 2.0
516     */
517    public void setCheckStrokeControlHint(boolean check) {
518        this.checkStrokeControlHint = check;
519    }
520    
521    /**
522     * Returns the prefix used for all keys in the DEFS element.  The default
523     * value is {@code String.valueOf(System.nanoTime())}.
524     * 
525     * @return The prefix string (never {@code null}).
526     * 
527     * @since 1.9
528     */
529    public String getDefsKeyPrefix() {
530        return this.defsKeyPrefix;
531    }
532    
533    /**
534     * Sets the prefix that will be used for all keys in the DEFS element.
535     * If required, this must be set immediately after construction (before any 
536     * content generation methods have been called).
537     * 
538     * @param prefix  the prefix ({@code null} not permitted).
539     * 
540     * @since 1.9
541     */
542    public void setDefsKeyPrefix(String prefix) {
543        Args.nullNotPermitted(prefix, "prefix");
544        this.defsKeyPrefix = prefix;
545    }
546    
547    /**
548     * Returns the number of decimal places used to write the transformation
549     * matrices in the SVG output.  The default value is 6.
550     * <p>
551     * Note that there is a separate attribute to control the number of decimal
552     * places for geometrical elements in the output (see 
553     * {@link #getGeometryDP()}).
554     * 
555     * @return The number of decimal places.
556     * 
557     * @see #setTransformDP(int) 
558     */
559    public int getTransformDP() {
560        return this.transformDP;    
561    }
562    
563    /**
564     * Sets the number of decimal places used to write the transformation
565     * matrices in the SVG output.  Values in the range 1 to 10 will be used
566     * to configure a formatter to that number of decimal places, for all other
567     * values we revert to the normal {@code String} conversion of 
568     * {@code double} primitives (approximately 16 decimals places).
569     * <p>
570     * Note that there is a separate attribute to control the number of decimal
571     * places for geometrical elements in the output (see 
572     * {@link #setGeometryDP(int)}).
573     * 
574     * @param dp  the number of decimal places (normally 1 to 10).
575     * 
576     * @see #getTransformDP() 
577     */
578    public void setTransformDP(int dp) {
579        this.transformDP = dp;
580        if (dp < 1 || dp > 10) {
581            this.transformFormat = null;
582            return;
583        }
584        DecimalFormatSymbols dfs = new DecimalFormatSymbols();
585        dfs.setDecimalSeparator('.');
586        this.transformFormat = new DecimalFormat("0." 
587                + "##########".substring(0, dp), dfs);
588    }
589    
590    /**
591     * Returns the number of decimal places used to write the coordinates
592     * of geometrical shapes.  The default value is 2.
593     * <p>
594     * Note that there is a separate attribute to control the number of decimal
595     * places for transform matrices in the output (see 
596     * {@link #getTransformDP()}).
597     * 
598     * @return The number of decimal places.
599     */
600    public int getGeometryDP() {
601        return this.geometryDP;    
602    }
603    
604    /**
605     * Sets the number of decimal places used to write the coordinates of
606     * geometrical shapes in the SVG output.  Values in the range 1 to 10 will 
607     * be used to configure a formatter to that number of decimal places, for 
608     * all other values we revert to the normal String conversion of double 
609     * primitives (approximately 16 decimals places).
610     * <p>
611     * Note that there is a separate attribute to control the number of decimal
612     * places for transform matrices in the output (see 
613     * {@link #setTransformDP(int)}).
614     * 
615     * @param dp  the number of decimal places (normally 1 to 10). 
616     */
617    public void setGeometryDP(int dp) {
618        this.geometryDP = dp;
619        if (dp < 1 || dp > 10) {
620            this.geometryFormat = null;
621            return;
622        }
623        DecimalFormatSymbols dfs = new DecimalFormatSymbols();
624        dfs.setDecimalSeparator('.');
625        this.geometryFormat = new DecimalFormat("0." 
626                + "##########".substring(0, dp), dfs);
627    }
628    
629    /**
630     * Returns the prefix used to generate a filename for an image that is
631     * referenced from, rather than embedded in, the SVG element.
632     * 
633     * @return The file prefix (never {@code null}).
634     * 
635     * @since 1.5
636     */
637    public String getFilePrefix() {
638        return this.filePrefix;
639    }
640    
641    /**
642     * Sets the prefix used to generate a filename for any image that is
643     * referenced from the SVG element.
644     * 
645     * @param prefix  the new prefix ({@code null} not permitted).
646     * 
647     * @since 1.5
648     */
649    public void setFilePrefix(String prefix) {
650        Args.nullNotPermitted(prefix, "prefix");
651        this.filePrefix = prefix;
652    }
653
654    /**
655     * Returns the suffix used to generate a filename for an image that is
656     * referenced from, rather than embedded in, the SVG element.
657     * 
658     * @return The file suffix (never {@code null}).
659     * 
660     * @since 1.5
661     */
662    public String getFileSuffix() {
663        return this.fileSuffix;
664    }
665    
666    /**
667     * Sets the suffix used to generate a filename for any image that is
668     * referenced from the SVG element.
669     * 
670     * @param suffix  the new prefix ({@code null} not permitted).
671     * 
672     * @since 1.5
673     */
674    public void setFileSuffix(String suffix) {
675        Args.nullNotPermitted(suffix, "suffix");
676        this.fileSuffix = suffix;
677    }
678    
679    /**
680     * Returns the width to use for the SVG stroke when the AWT stroke
681     * specified has a zero width (the default value is {@code 0.1}).  In 
682     * the Java specification for {@code BasicStroke} it states "If width 
683     * is set to 0.0f, the stroke is rendered as the thinnest possible 
684     * line for the target device and the antialias hint setting."  We don't 
685     * have a means to implement that accurately since we must specify a fixed
686     * width.
687     * 
688     * @return The width.
689     * 
690     * @since 1.9
691     */
692    public double getZeroStrokeWidth() {
693        return this.zeroStrokeWidth;
694    }
695    
696    /**
697     * Sets the width to use for the SVG stroke when the current AWT stroke
698     * has a width of 0.0.
699     * 
700     * @param width  the new width (must be 0 or greater).
701     * 
702     * @since 1.9
703     */
704    public void setZeroStrokeWidth(double width) {
705        if (width < 0.0) {
706            throw new IllegalArgumentException("Width cannot be negative.");
707        }
708        this.zeroStrokeWidth = width;
709    }
710 
711    /**
712     * Returns the device configuration associated with this
713     * {@code Graphics2D}.
714     * 
715     * @return The graphics configuration.
716     */
717    @Override
718    public GraphicsConfiguration getDeviceConfiguration() {
719        if (this.deviceConfiguration == null) {
720            this.deviceConfiguration = new SVGGraphicsConfiguration(this.width,
721                    this.height);
722        }
723        return this.deviceConfiguration;
724    }
725
726    /**
727     * Creates a new graphics object that is a copy of this graphics object
728     * (except that it has not accumulated the drawing operations).  Not sure
729     * yet when or why this would be useful when creating SVG output.
730     * 
731     * @return A new graphics object.
732     */
733    @Override
734    public Graphics create() {
735        SVGGraphics2D copy = new SVGGraphics2D(this.width, this.height, 
736                this.sb);
737        copy.setRenderingHints(getRenderingHints());
738        copy.setTransform(getTransform());
739        copy.setClip(getClip());
740        copy.setPaint(getPaint());
741        copy.setColor(getColor());
742        copy.setComposite(getComposite());
743        copy.setStroke(getStroke());
744        copy.setFont(getFont());
745        copy.setBackground(getBackground());
746        copy.setFilePrefix(getFilePrefix());
747        copy.setFileSuffix(getFileSuffix());
748        return copy;
749    }
750
751    /**
752     * Returns the paint used to draw or fill shapes (or text).  The default 
753     * value is {@link Color#BLACK}.
754     * 
755     * @return The paint (never {@code null}). 
756     * 
757     * @see #setPaint(java.awt.Paint) 
758     */
759    @Override
760    public Paint getPaint() {
761        return this.paint;
762    }
763    
764    /**
765     * Sets the paint used to draw or fill shapes (or text).  If 
766     * {@code paint} is an instance of {@code Color}, this method will
767     * also update the current color attribute (see {@link #getColor()}). If 
768     * you pass {@code null} to this method, it does nothing (in 
769     * accordance with the JDK specification).
770     * 
771     * @param paint  the paint ({@code null} is permitted but ignored).
772     * 
773     * @see #getPaint() 
774     */
775    @Override
776    public void setPaint(Paint paint) {
777        if (paint == null) {
778            return;
779        }
780        this.paint = paint;
781        this.gradientPaintRef = null;
782        if (paint instanceof Color) {
783            setColor((Color) paint);
784        } else if (paint instanceof GradientPaint) {
785            GradientPaint gp = (GradientPaint) paint;
786            GradientPaintKey key = new GradientPaintKey(gp);
787            String ref = this.gradientPaints.get(key);
788            if (ref == null) {
789                int count = this.gradientPaints.keySet().size();
790                String id = this.defsKeyPrefix + "gp" + count;
791                this.elementIDs.add(id);
792                this.gradientPaints.put(key, id);
793                this.gradientPaintRef = id;
794            } else {
795                this.gradientPaintRef = ref;
796            }
797        } else if (paint instanceof LinearGradientPaint) {
798            LinearGradientPaint lgp = (LinearGradientPaint) paint;
799            LinearGradientPaintKey key = new LinearGradientPaintKey(lgp);
800            String ref = this.linearGradientPaints.get(key);
801            if (ref == null) {
802                int count = this.linearGradientPaints.keySet().size();
803                String id = this.defsKeyPrefix + "lgp" + count;
804                this.elementIDs.add(id);
805                this.linearGradientPaints.put(key, id);
806                this.gradientPaintRef = id;
807            }
808        } else if (paint instanceof RadialGradientPaint) {
809            RadialGradientPaint rgp = (RadialGradientPaint) paint;
810            RadialGradientPaintKey key = new RadialGradientPaintKey(rgp);
811            String ref = this.radialGradientPaints.get(key);
812            if (ref == null) {
813                int count = this.radialGradientPaints.keySet().size();
814                String id = this.defsKeyPrefix + "rgp" + count;
815                this.elementIDs.add(id);
816                this.radialGradientPaints.put(key, id);
817                this.gradientPaintRef = id;
818            }
819        }
820    }
821
822    /**
823     * Returns the foreground color.  This method exists for backwards
824     * compatibility in AWT, you should use the {@link #getPaint()} method.
825     * 
826     * @return The foreground color (never {@code null}).
827     * 
828     * @see #getPaint() 
829     */
830    @Override
831    public Color getColor() {
832        return this.color;
833    }
834
835    /**
836     * Sets the foreground color.  This method exists for backwards 
837     * compatibility in AWT, you should use the 
838     * {@link #setPaint(java.awt.Paint)} method.
839     * 
840     * @param c  the color ({@code null} permitted but ignored). 
841     * 
842     * @see #setPaint(java.awt.Paint) 
843     */
844    @Override
845    public void setColor(Color c) {
846        if (c == null) {
847            return;
848        }
849        this.color = c;
850        this.paint = c;
851    }
852
853    /**
854     * Returns the background color.  The default value is {@link Color#BLACK}.
855     * This is used by the {@link #clearRect(int, int, int, int)} method.
856     * 
857     * @return The background color (possibly {@code null}). 
858     * 
859     * @see #setBackground(java.awt.Color) 
860     */
861    @Override
862    public Color getBackground() {
863        return this.background;
864    }
865
866    /**
867     * Sets the background color.  This is used by the 
868     * {@link #clearRect(int, int, int, int)} method.  The reference 
869     * implementation allows {@code null} for the background color so
870     * we allow that too (but for that case, the clearRect method will do 
871     * nothing).
872     * 
873     * @param color  the color ({@code null} permitted).
874     * 
875     * @see #getBackground() 
876     */
877    @Override
878    public void setBackground(Color color) {
879        this.background = color;
880    }
881
882    /**
883     * Returns the current composite.
884     * 
885     * @return The current composite (never {@code null}).
886     * 
887     * @see #setComposite(java.awt.Composite) 
888     */
889    @Override
890    public Composite getComposite() {
891        return this.composite;
892    }
893    
894    /**
895     * Sets the composite (only {@code AlphaComposite} is handled).
896     * 
897     * @param comp  the composite ({@code null} not permitted).
898     * 
899     * @see #getComposite() 
900     */
901    @Override
902    public void setComposite(Composite comp) {
903        if (comp == null) {
904            throw new IllegalArgumentException("Null 'comp' argument.");
905        }
906        this.composite = comp;
907    }
908
909    /**
910     * Returns the current stroke (used when drawing shapes). 
911     * 
912     * @return The current stroke (never {@code null}). 
913     * 
914     * @see #setStroke(java.awt.Stroke) 
915     */
916    @Override
917    public Stroke getStroke() {
918        return this.stroke;
919    }
920
921    /**
922     * Sets the stroke that will be used to draw shapes.
923     * 
924     * @param s  the stroke ({@code null} not permitted).
925     * 
926     * @see #getStroke() 
927     */
928    @Override
929    public void setStroke(Stroke s) {
930        if (s == null) {
931            throw new IllegalArgumentException("Null 's' argument.");
932        }
933        this.stroke = s;
934    }
935
936    /**
937     * Returns the current value for the specified hint.  See the 
938     * {@link SVGHints} class for information about the hints that can be
939     * used with {@code SVGGraphics2D}.
940     * 
941     * @param hintKey  the hint key ({@code null} permitted, but the
942     *     result will be {@code null} also).
943     * 
944     * @return The current value for the specified hint 
945     *     (possibly {@code null}).
946     * 
947     * @see #setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object) 
948     */
949    @Override
950    public Object getRenderingHint(RenderingHints.Key hintKey) {
951        return this.hints.get(hintKey);
952    }
953
954    /**
955     * Sets the value for a hint.  See the {@link SVGHints} class for 
956     * information about the hints that can be used with this implementation.
957     * 
958     * @param hintKey  the hint key ({@code null} not permitted).
959     * @param hintValue  the hint value.
960     * 
961     * @see #getRenderingHint(java.awt.RenderingHints.Key) 
962     */
963    @Override
964    public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
965        if (hintKey == null) {
966            throw new NullPointerException("Null 'hintKey' not permitted.");
967        }
968        // KEY_BEGIN_GROUP and KEY_END_GROUP are handled as special cases that
969        // never get stored in the hints map...
970        if (SVGHints.isBeginGroupKey(hintKey)) {
971            String groupId = null;
972            String ref = null;
973            List<Entry> otherKeysAndValues = null;
974            if (hintValue instanceof String) {
975                groupId = (String) hintValue;
976             } else if (hintValue instanceof Map) {
977                Map hintValueMap = (Map) hintValue;
978                groupId = (String) hintValueMap.get("id");
979                ref = (String) hintValueMap.get("ref");
980                for (final Object obj: hintValueMap.entrySet()) {
981                   final Entry e = (Entry) obj;
982                   final Object key = e.getKey();
983                   if ("id".equals(key) || "ref".equals(key)) {
984                      continue;
985                   }
986                   if (otherKeysAndValues == null) {
987                      otherKeysAndValues = new ArrayList<Entry>();
988                   }
989                   otherKeysAndValues.add(e);
990                }
991            }
992            this.sb.append("<g");
993            if (groupId != null) {
994                if (this.elementIDs.contains(groupId)) {
995                    throw new IllegalArgumentException("The group id (" 
996                            + groupId + ") is not unique.");
997                } else {
998                    this.sb.append(" id=\"").append(groupId).append("\"");
999                    this.elementIDs.add(groupId);
1000                }
1001            }
1002            if (ref != null) {
1003                this.sb.append(" jfreesvg:ref=\"");
1004                this.sb.append(SVGUtils.escapeForXML(ref)).append("\"");
1005            }
1006            if (otherKeysAndValues != null) {
1007               for (final Entry e: otherKeysAndValues) {
1008                    this.sb.append(" ").append(e.getKey()).append("=\"");
1009                    this.sb.append(SVGUtils.escapeForXML(String.valueOf(
1010                            e.getValue()))).append("\"");
1011               }
1012            }
1013            this.sb.append(">");
1014        } else if (SVGHints.isEndGroupKey(hintKey)) {
1015            this.sb.append("</g>\n");
1016        } else if (SVGHints.isElementTitleKey(hintKey) && (hintValue != null)) {
1017            this.sb.append("<title>");
1018            this.sb.append(SVGUtils.escapeForXML(String.valueOf(hintValue)));
1019            this.sb.append("</title>");     
1020        } else {
1021            this.hints.put(hintKey, hintValue);
1022        }
1023    }
1024
1025    /**
1026     * Returns a copy of the rendering hints.  Modifying the returned copy
1027     * will have no impact on the state of this {@code Graphics2D} instance.
1028     * 
1029     * @return The rendering hints (never {@code null}).
1030     * 
1031     * @see #setRenderingHints(java.util.Map) 
1032     */
1033    @Override
1034    public RenderingHints getRenderingHints() {
1035        return (RenderingHints) this.hints.clone();
1036    }
1037
1038    /**
1039     * Sets the rendering hints to the specified collection.
1040     * 
1041     * @param hints  the new set of hints ({@code null} not permitted).
1042     * 
1043     * @see #getRenderingHints() 
1044     */
1045    @Override
1046    public void setRenderingHints(Map<?, ?> hints) {
1047        this.hints.clear();
1048        addRenderingHints(hints);
1049    }
1050
1051    /**
1052     * Adds all the supplied rendering hints.
1053     * 
1054     * @param hints  the hints ({@code null} not permitted).
1055     */
1056    @Override
1057    public void addRenderingHints(Map<?, ?> hints) {
1058        this.hints.putAll(hints);
1059    }
1060
1061    /**
1062     * A utility method that appends an optional element id if one is 
1063     * specified via the rendering hints.
1064     * 
1065     * @param sb  the string builder ({@code null} not permitted). 
1066     */
1067    private void appendOptionalElementIDFromHint(StringBuilder sb) {
1068        String elementID = (String) this.hints.get(SVGHints.KEY_ELEMENT_ID);
1069        if (elementID != null) {
1070            this.hints.put(SVGHints.KEY_ELEMENT_ID, null); // clear it
1071            if (this.elementIDs.contains(elementID)) {
1072                throw new IllegalStateException("The element id " 
1073                        + elementID + " is already used.");
1074            } else {
1075                this.elementIDs.add(elementID);
1076            }
1077            this.sb.append("id=\"").append(elementID).append("\" ");
1078        }
1079    }
1080    
1081    /**
1082     * Draws the specified shape with the current {@code paint} and 
1083     * {@code stroke}.  There is direct handling for {@code Line2D}, 
1084     * {@code Rectangle2D} and {@code Path2D}. All other shapes are
1085     * mapped to a {@code GeneralPath} and then drawn (effectively as 
1086     * {@code Path2D} objects).
1087     * 
1088     * @param s  the shape ({@code null} not permitted).
1089     * 
1090     * @see #fill(java.awt.Shape) 
1091     */
1092    @Override
1093    public void draw(Shape s) {
1094        // if the current stroke is not a BasicStroke then it is handled as
1095        // a special case
1096        if (!(this.stroke instanceof BasicStroke)) {
1097            fill(this.stroke.createStrokedShape(s));
1098            return;
1099        }
1100        if (s instanceof Line2D) {
1101            Line2D l = (Line2D) s;
1102            this.sb.append("<line ");
1103            appendOptionalElementIDFromHint(this.sb);
1104            this.sb.append("x1=\"").append(geomDP(l.getX1()))
1105                    .append("\" y1=\"").append(geomDP(l.getY1()))
1106                    .append("\" x2=\"").append(geomDP(l.getX2()))
1107                    .append("\" y2=\"").append(geomDP(l.getY2()))
1108                    .append("\" ");
1109            this.sb.append("style=\"").append(strokeStyle()).append("\" ");
1110            this.sb.append("transform=\"").append(getSVGTransform(
1111                    this.transform)).append("\" ");
1112            this.sb.append(getClipPathRef());
1113            this.sb.append("/>");
1114        } else if (s instanceof Rectangle2D) {
1115            Rectangle2D r = (Rectangle2D) s;
1116            this.sb.append("<rect ");
1117            appendOptionalElementIDFromHint(this.sb);
1118            this.sb.append("x=\"").append(geomDP(r.getX()))
1119                    .append("\" y=\"").append(geomDP(r.getY()))
1120                    .append("\" width=\"").append(geomDP(r.getWidth()))
1121                    .append("\" height=\"").append(geomDP(r.getHeight()))
1122                    .append("\" ");
1123            this.sb.append("style=\"").append(strokeStyle())
1124                    .append("; fill: none").append("\" ");
1125            this.sb.append("transform=\"").append(getSVGTransform(
1126                    this.transform)).append("\" ");
1127            this.sb.append(getClipPathRef());
1128            this.sb.append("/>");
1129        } else if (s instanceof Path2D) {
1130            Path2D path = (Path2D) s;
1131            this.sb.append("<g ");
1132            appendOptionalElementIDFromHint(this.sb);
1133            this.sb.append("style=\"").append(strokeStyle())
1134                    .append("; fill: none").append("\" ");
1135            this.sb.append("transform=\"").append(getSVGTransform(
1136                    this.transform)).append("\" ");
1137            this.sb.append(getClipPathRef());
1138            this.sb.append(">");
1139            this.sb.append("<path ").append(getSVGPathData(path)).append("/>");
1140            this.sb.append("</g>");
1141        } else {
1142            draw(new GeneralPath(s)); // handled as a Path2D next time through
1143        }
1144    }
1145
1146    /**
1147     * Fills the specified shape with the current {@code paint}.  There is
1148     * direct handling for {@code Rectangle2D} and {@code Path2D}.  
1149     * All other shapes are mapped to a {@code GeneralPath} and then 
1150     * filled.
1151     * 
1152     * @param s  the shape ({@code null} not permitted). 
1153     * 
1154     * @see #draw(java.awt.Shape) 
1155     */
1156    @Override
1157    public void fill(Shape s) {
1158        if (s instanceof Rectangle2D) {
1159            Rectangle2D r = (Rectangle2D) s;
1160            if (r.isEmpty()) {
1161                return;
1162            }
1163            this.sb.append("<rect ");
1164            appendOptionalElementIDFromHint(this.sb);
1165            this.sb.append("x=\"").append(geomDP(r.getX()))
1166                    .append("\" y=\"").append(geomDP(r.getY()))
1167                    .append("\" width=\"").append(geomDP(r.getWidth()))
1168                    .append("\" height=\"").append(geomDP(r.getHeight()))
1169                    .append("\" ");
1170            this.sb.append("style=\"").append(getSVGFillStyle()).append("\" ");
1171            this.sb.append("transform=\"").append(getSVGTransform(
1172                    this.transform)).append("\" ");
1173            this.sb.append(getClipPathRef());
1174            this.sb.append("/>");
1175        } else if (s instanceof Path2D) {
1176            Path2D path = (Path2D) s;
1177            this.sb.append("<g ");
1178            appendOptionalElementIDFromHint(this.sb);
1179            this.sb.append("style=\"").append(getSVGFillStyle());
1180            this.sb.append("; stroke: none").append("\" ");
1181            this.sb.append("transform=\"").append(getSVGTransform(
1182                    this.transform)).append("\" ");
1183            this.sb.append(getClipPathRef());
1184            this.sb.append(">");
1185            this.sb.append("<path ").append(getSVGPathData(path)).append("/>");
1186            this.sb.append("</g>");
1187        }  else {
1188            fill(new GeneralPath(s));  // handled as a Path2D next time through
1189        }
1190    }
1191    
1192    /**
1193     * Creates an SVG path string for the supplied Java2D path.
1194     * 
1195     * @param path  the path ({@code null} not permitted).
1196     * 
1197     * @return An SVG path string. 
1198     */
1199    private String getSVGPathData(Path2D path) {
1200        StringBuilder b = new StringBuilder("d=\"");
1201        float[] coords = new float[6];
1202        double[] closePt = null;
1203        boolean first = true;
1204        PathIterator iterator = path.getPathIterator(null);
1205        while (!iterator.isDone()) {
1206            int type = iterator.currentSegment(coords);
1207            if (!first) {
1208                b.append(" ");
1209            }
1210            first = false;
1211            switch (type) {
1212            case (PathIterator.SEG_MOVETO):
1213                closePt = new double[2];
1214                closePt[0] = coords[0];
1215                closePt[1] = coords[1];
1216                b.append("M ").append(geomDP(coords[0])).append(" ")
1217                        .append(geomDP(coords[1]));
1218                break;
1219            case (PathIterator.SEG_LINETO):
1220                b.append("L ").append(geomDP(coords[0])).append(" ")
1221                        .append(geomDP(coords[1]));
1222                break;
1223            case (PathIterator.SEG_QUADTO):
1224                b.append("Q ").append(geomDP(coords[0]))
1225                        .append(" ").append(geomDP(coords[1]))
1226                        .append(" ").append(geomDP(coords[2]))
1227                        .append(" ").append(geomDP(coords[3]));
1228                break;
1229            case (PathIterator.SEG_CUBICTO):
1230                b.append("C ").append(geomDP(coords[0])).append(" ")
1231                        .append(geomDP(coords[1])).append(" ")
1232                        .append(geomDP(coords[2])).append(" ")
1233                        .append(geomDP(coords[3])).append(" ")
1234                        .append(geomDP(coords[4])).append(" ")
1235                        .append(geomDP(coords[5]));
1236                break;
1237            case (PathIterator.SEG_CLOSE):
1238                if (closePt != null) {
1239                    b.append("M ").append(geomDP(closePt[0])).append(" ")
1240                            .append(geomDP(closePt[1]));
1241                }
1242                break;
1243            default:
1244                break;
1245            }
1246            iterator.next();
1247        }  
1248        return b.append("\"").toString();
1249    }
1250
1251    /**
1252     * Returns the current alpha (transparency) in the range 0.0 to 1.0.
1253     * If the current composite is an {@link AlphaComposite} we read the alpha
1254     * value from there, otherwise this method returns 1.0.
1255     * 
1256     * @return The current alpha (transparency) in the range 0.0 to 1.0.
1257     */
1258    private float getAlpha() {
1259       float alpha = 1.0f;
1260       if (this.composite instanceof AlphaComposite) {
1261           AlphaComposite ac = (AlphaComposite) this.composite;
1262           alpha = ac.getAlpha();
1263       }
1264       return alpha;
1265    }
1266
1267    /**
1268     * Returns an SVG color string based on the current paint.  To handle
1269     * {@code GradientPaint} we rely on the {@code setPaint()} method
1270     * having set the {@code gradientPaintRef} attribute.
1271     * 
1272     * @return An SVG color string. 
1273     */
1274    private String svgColorStr() {
1275        String result = "black;";
1276        if (this.paint instanceof Color) {
1277            return rgbColorStr((Color) this.paint);
1278        } else if (this.paint instanceof GradientPaint 
1279                || this.paint instanceof LinearGradientPaint
1280                || this.paint instanceof RadialGradientPaint) {
1281            return "url(#" + this.gradientPaintRef + ")";
1282        }
1283        return result;
1284    }
1285    
1286    /**
1287     * Returns the SVG RGB color string for the specified color.
1288     * 
1289     * @param c  the color ({@code null} not permitted).
1290     * 
1291     * @return The SVG RGB color string.
1292     */
1293    private String rgbColorStr(Color c) {
1294        StringBuilder b = new StringBuilder("rgb(");
1295        b.append(c.getRed()).append(",").append(c.getGreen()).append(",")
1296                .append(c.getBlue()).append(")");
1297        return b.toString();
1298    }
1299    
1300    /**
1301     * Returns a string representing the specified color in RGBA format.
1302     * 
1303     * @param c  the color ({@code null} not permitted).
1304     * 
1305     * @return The SVG RGBA color string.
1306     */
1307    private String rgbaColorStr(Color c) {
1308        StringBuilder b = new StringBuilder("rgba(");
1309        double alphaPercent = c.getAlpha() / 255.0;
1310        b.append(c.getRed()).append(",").append(c.getGreen()).append(",")
1311                .append(c.getBlue());
1312        b.append(",").append(transformDP(alphaPercent));
1313        b.append(")");
1314        return b.toString();
1315    }
1316    
1317    /**
1318     * Returns a stroke style string based on the current stroke and
1319     * alpha settings.
1320     * 
1321     * @return A stroke style string.
1322     */
1323    private String strokeStyle() {
1324        double strokeWidth = 1.0f;
1325        float[] dashArray = new float[0];
1326        if (this.stroke instanceof BasicStroke) {
1327            // in fact this method doesn't get called for other stroke types
1328            BasicStroke bs = (BasicStroke) this.stroke;
1329            strokeWidth = bs.getLineWidth() > 0.0 ? bs.getLineWidth() 
1330                    : this.zeroStrokeWidth;
1331            dashArray = bs.getDashArray();
1332        }
1333        StringBuilder b = new StringBuilder();
1334        b.append("stroke-width: ").append(strokeWidth).append(";");
1335        b.append("stroke: ").append(svgColorStr()).append(";");
1336        b.append("stroke-opacity: ").append(getColorAlpha() * getAlpha())
1337                .append(";");
1338        if (dashArray != null && dashArray.length != 0) {
1339            b.append("stroke-dasharray: ");
1340            for (int i = 0; i < dashArray.length; i++) {
1341                if (i != 0) b.append(", ");
1342                b.append(dashArray[i]);
1343            }
1344            b.append(";");
1345        }
1346        if (this.checkStrokeControlHint) {
1347            Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
1348            if (RenderingHints.VALUE_STROKE_NORMALIZE.equals(hint) 
1349                    && !this.shapeRendering.equals("crispEdges")) {
1350                b.append("shape-rendering:crispEdges;");
1351            }
1352            if (RenderingHints.VALUE_STROKE_PURE.equals(hint) 
1353                    && !this.shapeRendering.equals("geometricPrecision")) {
1354                b.append("shape-rendering:geometricPrecision;");
1355            }
1356        }
1357        return b.toString();
1358    }
1359    
1360    /**
1361     * Returns the alpha value of the current {@code paint}, or {@code 1.0f} if
1362     * it is not an instance of {@code Color}.
1363     * 
1364     * @return The alpha value (in the range {@code 0.0} to {@code 1.0}. 
1365     */
1366    private float getColorAlpha() {
1367        if (this.paint instanceof Color) {
1368            Color c = (Color) this.paint;
1369            return c.getAlpha() / 255.0f; 
1370        } 
1371        return 1f;
1372    }
1373    
1374    /**
1375     * Returns a fill style string based on the current paint and
1376     * alpha settings.
1377     * 
1378     * @return A fill style string.
1379     */
1380    private String getSVGFillStyle() {
1381        StringBuilder b = new StringBuilder();
1382        b.append("fill: ").append(svgColorStr()).append("; ");
1383        b.append("fill-opacity: ").append(getColorAlpha() * getAlpha());
1384        return b.toString();
1385    }
1386
1387    /**
1388     * Returns the current font used for drawing text.
1389     * 
1390     * @return The current font (never {@code null}).
1391     * 
1392     * @see #setFont(java.awt.Font) 
1393     */
1394    @Override
1395    public Font getFont() {
1396        return this.font;
1397    }
1398
1399    /**
1400     * Sets the font to be used for drawing text.
1401     * 
1402     * @param font  the font ({@code null} is permitted but ignored).
1403     * 
1404     * @see #getFont() 
1405     */
1406    @Override
1407    public void setFont(Font font) {
1408        if (font == null) {
1409            return;
1410        }
1411        this.font = font;
1412    }
1413    
1414    /**
1415     * Returns the font mapper (an object that optionally maps font family
1416     * names to alternates).  The default mapper will convert Java logical 
1417     * font names to the equivalent SVG generic font name, and leave all other
1418     * font names unchanged.
1419     * 
1420     * @return The font mapper (never {@code null}).
1421     * 
1422     * @see #setFontMapper(org.jfree.graphics2d.svg.FontMapper) 
1423     * @since 1.5
1424     */
1425    public FontMapper getFontMapper() {
1426        return this.fontMapper;
1427    }
1428    
1429    /**
1430     * Sets the font mapper.
1431     * 
1432     * @param mapper  the font mapper ({@code null} not permitted).
1433     * 
1434     * @since 1.5
1435     */
1436    public void setFontMapper(FontMapper mapper) {
1437        Args.nullNotPermitted(mapper, "mapper");
1438        this.fontMapper = mapper;
1439    }
1440    
1441    /**
1442     * Returns a string containing font style info.
1443     * 
1444     * @return A string containing font style info.
1445     */
1446    private String getSVGFontStyle() {
1447        StringBuilder b = new StringBuilder();
1448        b.append("fill: ").append(svgColorStr()).append("; ");
1449        b.append("fill-opacity: ").append(getColorAlpha() * getAlpha())
1450                .append("; ");
1451        String fontFamily = this.fontMapper.mapFont(this.font.getFamily());
1452        b.append("font-family: ").append(fontFamily).append("; ");
1453        b.append("font-size: ").append(this.font.getSize()).append("px; ");
1454        if (this.font.isBold()) {
1455            b.append("font-weight: bold; ");
1456        }
1457        if (this.font.isItalic()) {
1458            b.append("font-style: italic; ");
1459        }
1460        return b.toString();
1461    }
1462
1463    /**
1464     * Returns the font metrics for the specified font.
1465     * 
1466     * @param f  the font.
1467     * 
1468     * @return The font metrics. 
1469     */
1470    @Override
1471    public FontMetrics getFontMetrics(Font f) {
1472        return this.fmImage.createGraphics().getFontMetrics(f);
1473    }
1474    
1475    /**
1476     * Returns the font render context.
1477     * 
1478     * @return The font render context (never {@code null}).
1479     */
1480    @Override
1481    public FontRenderContext getFontRenderContext() {
1482        return this.fontRenderContext;
1483    }
1484
1485    /**
1486     * Draws a string at {@code (x, y)}.  The start of the text at the
1487     * baseline level will be aligned with the {@code (x, y)} point.
1488     * <br><br>
1489     * Note that you can make use of the {@link SVGHints#KEY_TEXT_RENDERING} 
1490     * hint when drawing strings (this is completely optional though). 
1491     * 
1492     * @param str  the string ({@code null} not permitted).
1493     * @param x  the x-coordinate.
1494     * @param y  the y-coordinate.
1495     * 
1496     * @see #drawString(java.lang.String, float, float) 
1497     */
1498    @Override
1499    public void drawString(String str, int x, int y) {
1500        drawString(str, (float) x, (float) y);
1501    }
1502
1503    /**
1504     * Draws a string at {@code (x, y)}. The start of the text at the
1505     * baseline level will be aligned with the {@code (x, y)} point.
1506     * <br><br>
1507     * Note that you can make use of the {@link SVGHints#KEY_TEXT_RENDERING} 
1508     * hint when drawing strings (this is completely optional though). 
1509     * 
1510     * @param str  the string ({@code null} not permitted).
1511     * @param x  the x-coordinate.
1512     * @param y  the y-coordinate.
1513     */
1514    @Override
1515    public void drawString(String str, float x, float y) {
1516        if (str == null) {
1517            throw new NullPointerException("Null 'str' argument.");
1518        }
1519        if (!SVGHints.VALUE_DRAW_STRING_TYPE_VECTOR.equals(
1520                this.hints.get(SVGHints.KEY_DRAW_STRING_TYPE))) {
1521            this.sb.append("<g ");
1522            appendOptionalElementIDFromHint(this.sb);
1523            this.sb.append("transform=\"").append(getSVGTransform(
1524                    this.transform)).append("\">");
1525            this.sb.append("<text x=\"").append(geomDP(x))
1526                    .append("\" y=\"").append(geomDP(y))
1527                    .append("\"");
1528            this.sb.append(" style=\"").append(getSVGFontStyle()).append("\"");
1529            Object hintValue = getRenderingHint(SVGHints.KEY_TEXT_RENDERING);
1530            if (hintValue != null) {
1531                String textRenderValue = hintValue.toString();
1532                this.sb.append(" text-rendering=\"").append(textRenderValue)
1533                        .append("\"");
1534            }
1535            this.sb.append(" ").append(getClipPathRef());
1536            this.sb.append(">");
1537            this.sb.append(SVGUtils.escapeForXML(str)).append("</text>");
1538            this.sb.append("</g>");
1539        } else {
1540            AttributedString as = new AttributedString(str, 
1541                    this.font.getAttributes());
1542            drawString(as.getIterator(), x, y);
1543        }
1544    }
1545
1546    /**
1547     * Draws a string of attributed characters at {@code (x, y)}.  The 
1548     * call is delegated to 
1549     * {@link #drawString(AttributedCharacterIterator, float, float)}. 
1550     * 
1551     * @param iterator  an iterator for the characters.
1552     * @param x  the x-coordinate.
1553     * @param y  the x-coordinate.
1554     */
1555    @Override
1556    public void drawString(AttributedCharacterIterator iterator, int x, int y) {
1557        drawString(iterator, (float) x, (float) y); 
1558    }
1559
1560    /**
1561     * Draws a string of attributed characters at {@code (x, y)}. 
1562     * 
1563     * @param iterator  an iterator over the characters ({@code null} not 
1564     *     permitted).
1565     * @param x  the x-coordinate.
1566     * @param y  the y-coordinate.
1567     */
1568    @Override
1569    public void drawString(AttributedCharacterIterator iterator, float x, 
1570            float y) {
1571        Set<Attribute> s = iterator.getAllAttributeKeys();
1572        if (!s.isEmpty()) {
1573            TextLayout layout = new TextLayout(iterator, 
1574                    getFontRenderContext());
1575            layout.draw(this, x, y);
1576        } else {
1577            StringBuilder strb = new StringBuilder();
1578            iterator.first();
1579            for (int i = iterator.getBeginIndex(); i < iterator.getEndIndex(); 
1580                    i++) {
1581                strb.append(iterator.current());
1582                iterator.next();
1583            }
1584            drawString(strb.toString(), x, y);
1585        }
1586    }
1587
1588    /**
1589     * Draws the specified glyph vector at the location {@code (x, y)}.
1590     * 
1591     * @param g  the glyph vector ({@code null} not permitted).
1592     * @param x  the x-coordinate.
1593     * @param y  the y-coordinate.
1594     */
1595    @Override
1596    public void drawGlyphVector(GlyphVector g, float x, float y) {
1597        fill(g.getOutline(x, y));
1598    }
1599
1600    /**
1601     * Applies the translation {@code (tx, ty)}.  This call is delegated 
1602     * to {@link #translate(double, double)}.
1603     * 
1604     * @param tx  the x-translation.
1605     * @param ty  the y-translation.
1606     * 
1607     * @see #translate(double, double) 
1608     */
1609    @Override
1610    public void translate(int tx, int ty) {
1611        translate((double) tx, (double) ty);
1612    }
1613
1614    /**
1615     * Applies the translation {@code (tx, ty)}.
1616     * 
1617     * @param tx  the x-translation.
1618     * @param ty  the y-translation.
1619     */
1620    @Override
1621    public void translate(double tx, double ty) {
1622        AffineTransform t = getTransform();
1623        t.translate(tx, ty);
1624        setTransform(t);
1625    }
1626
1627    /**
1628     * Applies a rotation (anti-clockwise) about {@code (0, 0)}.
1629     * 
1630     * @param theta  the rotation angle (in radians). 
1631     */
1632    @Override
1633    public void rotate(double theta) {
1634        AffineTransform t = getTransform();
1635        t.rotate(theta);
1636        setTransform(t);
1637    }
1638
1639    /**
1640     * Applies a rotation (anti-clockwise) about {@code (x, y)}.
1641     * 
1642     * @param theta  the rotation angle (in radians).
1643     * @param x  the x-coordinate.
1644     * @param y  the y-coordinate.
1645     */
1646    @Override
1647    public void rotate(double theta, double x, double y) {
1648        translate(x, y);
1649        rotate(theta);
1650        translate(-x, -y);
1651    }
1652
1653    /**
1654     * Applies a scale transformation.
1655     * 
1656     * @param sx  the x-scaling factor.
1657     * @param sy  the y-scaling factor.
1658     */
1659    @Override
1660    public void scale(double sx, double sy) {
1661        AffineTransform t = getTransform();
1662        t.scale(sx, sy);
1663        setTransform(t);
1664    }
1665
1666    /**
1667     * Applies a shear transformation. This is equivalent to the following 
1668     * call to the {@code transform} method:
1669     * <br><br>
1670     * <ul><li>
1671     * {@code transform(AffineTransform.getShearInstance(shx, shy));}
1672     * </ul>
1673     * 
1674     * @param shx  the x-shear factor.
1675     * @param shy  the y-shear factor.
1676     */
1677    @Override
1678    public void shear(double shx, double shy) {
1679        transform(AffineTransform.getShearInstance(shx, shy));
1680    }
1681
1682    /**
1683     * Applies this transform to the existing transform by concatenating it.
1684     * 
1685     * @param t  the transform ({@code null} not permitted). 
1686     */
1687    @Override
1688    public void transform(AffineTransform t) {
1689        AffineTransform tx = getTransform();
1690        tx.concatenate(t);
1691        setTransform(tx);
1692    }
1693
1694    /**
1695     * Returns a copy of the current transform.
1696     * 
1697     * @return A copy of the current transform (never {@code null}).
1698     * 
1699     * @see #setTransform(java.awt.geom.AffineTransform) 
1700     */
1701    @Override
1702    public AffineTransform getTransform() {
1703        return (AffineTransform) this.transform.clone();
1704    }
1705
1706    /**
1707     * Sets the transform.
1708     * 
1709     * @param t  the new transform ({@code null} permitted, resets to the
1710     *     identity transform).
1711     * 
1712     * @see #getTransform() 
1713     */
1714    @Override
1715    public void setTransform(AffineTransform t) {
1716        if (t == null) {
1717            this.transform = new AffineTransform();
1718        } else {
1719            this.transform = new AffineTransform(t);
1720        }
1721        this.clipRef = null;
1722    }
1723
1724    /**
1725     * Returns {@code true} if the rectangle (in device space) intersects
1726     * with the shape (the interior, if {@code onStroke} is {@code false}, 
1727     * otherwise the stroked outline of the shape).
1728     * 
1729     * @param rect  a rectangle (in device space).
1730     * @param s the shape.
1731     * @param onStroke  test the stroked outline only?
1732     * 
1733     * @return A boolean. 
1734     */
1735    @Override
1736    public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
1737        Shape ts;
1738        if (onStroke) {
1739            ts = this.transform.createTransformedShape(
1740                    this.stroke.createStrokedShape(s));
1741        } else {
1742            ts = this.transform.createTransformedShape(s);
1743        }
1744        if (!rect.getBounds2D().intersects(ts.getBounds2D())) {
1745            return false;
1746        }
1747        Area a1 = new Area(rect);
1748        Area a2 = new Area(ts);
1749        a1.intersect(a2);
1750        return !a1.isEmpty();
1751    }
1752
1753    /**
1754     * Does nothing in this {@code SVGGraphics2D} implementation.
1755     */
1756    @Override
1757    public void setPaintMode() {
1758        // do nothing
1759    }
1760
1761    /**
1762     * Does nothing in this {@code SVGGraphics2D} implementation.
1763     * 
1764     * @param c  ignored
1765     */
1766    @Override
1767    public void setXORMode(Color c) {
1768        // do nothing
1769    }
1770
1771    /**
1772     * Returns the bounds of the user clipping region.
1773     * 
1774     * @return The clip bounds (possibly {@code null}). 
1775     * 
1776     * @see #getClip() 
1777     */
1778    @Override
1779    public Rectangle getClipBounds() {
1780        if (this.clip == null) {
1781            return null;
1782        }
1783        return getClip().getBounds();
1784    }
1785
1786    /**
1787     * Returns the user clipping region.  The initial default value is 
1788     * {@code null}.
1789     * 
1790     * @return The user clipping region (possibly {@code null}).
1791     * 
1792     * @see #setClip(java.awt.Shape)
1793     */
1794    @Override
1795    public Shape getClip() {
1796        if (this.clip == null) {
1797            return null;
1798        }
1799        AffineTransform inv;
1800        try {
1801            inv = this.transform.createInverse();
1802            return inv.createTransformedShape(this.clip);
1803        } catch (NoninvertibleTransformException ex) {
1804            return null;
1805        }
1806    }
1807
1808    /**
1809     * Sets the user clipping region.
1810     * 
1811     * @param shape  the new user clipping region ({@code null} permitted).
1812     * 
1813     * @see #getClip()
1814     */
1815    @Override
1816    public void setClip(Shape shape) {
1817        // null is handled fine here...
1818        this.clip = this.transform.createTransformedShape(shape);
1819        this.clipRef = null;
1820    }
1821    
1822    /**
1823     * Registers the clip so that we can later write out all the clip 
1824     * definitions in the DEFS element.
1825     * 
1826     * @param clip  the clip (ignored if {@code null}) 
1827     */
1828    private String registerClip(Shape clip) {
1829        if (clip == null) {
1830            this.clipRef = null;
1831            return null;
1832        }
1833        // generate the path
1834        String pathStr = getSVGPathData(new Path2D.Double(clip));
1835        int index = this.clipPaths.indexOf(pathStr);
1836        if (index < 0) {
1837            this.clipPaths.add(pathStr);
1838            index = this.clipPaths.size() - 1;
1839        }
1840        return this.defsKeyPrefix + CLIP_KEY_PREFIX + index;
1841    }
1842    
1843    private String transformDP(double d) {
1844        if (this.transformFormat != null) {
1845            return transformFormat.format(d);            
1846        } else {
1847            return String.valueOf(d);
1848        }
1849    }
1850    
1851    private String geomDP(double d) {
1852        if (this.geometryFormat != null) {
1853            return geometryFormat.format(d);            
1854        } else {
1855            return String.valueOf(d);
1856        }
1857    }
1858    
1859    private String getSVGTransform(AffineTransform t) {
1860        StringBuilder b = new StringBuilder("matrix(");
1861        b.append(transformDP(t.getScaleX())).append(",");
1862        b.append(transformDP(t.getShearY())).append(",");
1863        b.append(transformDP(t.getShearX())).append(",");
1864        b.append(transformDP(t.getScaleY())).append(",");
1865        b.append(transformDP(t.getTranslateX())).append(",");
1866        b.append(transformDP(t.getTranslateY())).append(")");
1867        return b.toString();
1868    }
1869
1870    /**
1871     * Clips to the intersection of the current clipping region and the
1872     * specified shape. 
1873     * 
1874     * According to the Oracle API specification, this method will accept a 
1875     * {@code null} argument, but there is an open bug report (since 2004) 
1876     * that suggests this is wrong:
1877     * <p>
1878     * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189">
1879     * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189</a>
1880     * 
1881     * @param s  the clip shape ({@code null} not permitted). 
1882     */
1883    @Override
1884    public void clip(Shape s) {
1885        if (s instanceof Line2D) {
1886            s = s.getBounds2D();
1887        }
1888        if (this.clip == null) {
1889            setClip(s);
1890            return;
1891        }
1892        Shape ts = this.transform.createTransformedShape(s);
1893        if (!ts.intersects(this.clip.getBounds2D())) {
1894            setClip(new Rectangle2D.Double());
1895        } else {
1896          Area a1 = new Area(ts);
1897          Area a2 = new Area(this.clip);
1898          a1.intersect(a2);
1899          this.clip = new Path2D.Double(a1);
1900        }
1901        this.clipRef = null;
1902    }
1903
1904    /**
1905     * Clips to the intersection of the current clipping region and the 
1906     * specified rectangle.
1907     * 
1908     * @param x  the x-coordinate.
1909     * @param y  the y-coordinate.
1910     * @param width  the width.
1911     * @param height  the height.
1912     */
1913    @Override
1914    public void clipRect(int x, int y, int width, int height) {
1915        setRect(x, y, width, height);
1916        clip(this.rect);
1917    }
1918
1919    /**
1920     * Sets the user clipping region to the specified rectangle.
1921     * 
1922     * @param x  the x-coordinate.
1923     * @param y  the y-coordinate.
1924     * @param width  the width.
1925     * @param height  the height.
1926     * 
1927     * @see #getClip() 
1928     */
1929    @Override
1930    public void setClip(int x, int y, int width, int height) {
1931        setRect(x, y, width, height);
1932        setClip(this.rect);
1933    }
1934
1935    /**
1936     * Draws a line from {@code (x1, y1)} to {@code (x2, y2)} using 
1937     * the current {@code paint} and {@code stroke}.
1938     * 
1939     * @param x1  the x-coordinate of the start point.
1940     * @param y1  the y-coordinate of the start point.
1941     * @param x2  the x-coordinate of the end point.
1942     * @param y2  the x-coordinate of the end point.
1943     */
1944    @Override
1945    public void drawLine(int x1, int y1, int x2, int y2) {
1946        if (this.line == null) {
1947            this.line = new Line2D.Double(x1, y1, x2, y2);
1948        } else {
1949            this.line.setLine(x1, y1, x2, y2);
1950        }
1951        draw(this.line);
1952    }
1953
1954    /**
1955     * Fills the specified rectangle with the current {@code paint}.
1956     * 
1957     * @param x  the x-coordinate.
1958     * @param y  the y-coordinate.
1959     * @param width  the rectangle width.
1960     * @param height  the rectangle height.
1961     */
1962    @Override
1963    public void fillRect(int x, int y, int width, int height) {
1964        setRect(x, y, width, height);
1965        fill(this.rect);
1966    }
1967
1968    /**
1969     * Clears the specified rectangle by filling it with the current 
1970     * background color.  If the background color is {@code null}, this
1971     * method will do nothing.
1972     * 
1973     * @param x  the x-coordinate.
1974     * @param y  the y-coordinate.
1975     * @param width  the width.
1976     * @param height  the height.
1977     * 
1978     * @see #getBackground() 
1979     */
1980    @Override
1981    public void clearRect(int x, int y, int width, int height) {
1982        if (getBackground() == null) {
1983            return;  // we can't do anything
1984        }
1985        Paint saved = getPaint();
1986        setPaint(getBackground());
1987        fillRect(x, y, width, height);
1988        setPaint(saved);
1989    }
1990    
1991    /**
1992     * Draws a rectangle with rounded corners using the current 
1993     * {@code paint} and {@code stroke}.
1994     * 
1995     * @param x  the x-coordinate.
1996     * @param y  the y-coordinate.
1997     * @param width  the width.
1998     * @param height  the height.
1999     * @param arcWidth  the arc-width.
2000     * @param arcHeight  the arc-height.
2001     * 
2002     * @see #fillRoundRect(int, int, int, int, int, int) 
2003     */
2004    @Override
2005    public void drawRoundRect(int x, int y, int width, int height, 
2006            int arcWidth, int arcHeight) {
2007        setRoundRect(x, y, width, height, arcWidth, arcHeight);
2008        draw(this.roundRect);
2009    }
2010
2011    /**
2012     * Fills a rectangle with rounded corners using the current {@code paint}.
2013     * 
2014     * @param x  the x-coordinate.
2015     * @param y  the y-coordinate.
2016     * @param width  the width.
2017     * @param height  the height.
2018     * @param arcWidth  the arc-width.
2019     * @param arcHeight  the arc-height.
2020     * 
2021     * @see #drawRoundRect(int, int, int, int, int, int) 
2022     */
2023    @Override
2024    public void fillRoundRect(int x, int y, int width, int height, 
2025            int arcWidth, int arcHeight) {
2026        setRoundRect(x, y, width, height, arcWidth, arcHeight);
2027        fill(this.roundRect);
2028    }
2029
2030    /**
2031     * Draws an oval framed by the rectangle {@code (x, y, width, height)}
2032     * using the current {@code paint} and {@code stroke}.
2033     * 
2034     * @param x  the x-coordinate.
2035     * @param y  the y-coordinate.
2036     * @param width  the width.
2037     * @param height  the height.
2038     * 
2039     * @see #fillOval(int, int, int, int) 
2040     */
2041    @Override
2042    public void drawOval(int x, int y, int width, int height) {
2043        setOval(x, y, width, height);
2044        draw(this.oval);
2045    }
2046
2047    /**
2048     * Fills an oval framed by the rectangle {@code (x, y, width, height)}.
2049     * 
2050     * @param x  the x-coordinate.
2051     * @param y  the y-coordinate.
2052     * @param width  the width.
2053     * @param height  the height.
2054     * 
2055     * @see #drawOval(int, int, int, int) 
2056     */
2057    @Override
2058    public void fillOval(int x, int y, int width, int height) {
2059        setOval(x, y, width, height);
2060        fill(this.oval);
2061    }
2062
2063    /**
2064     * Draws an arc contained within the rectangle 
2065     * {@code (x, y, width, height)}, starting at {@code startAngle}
2066     * and continuing through {@code arcAngle} degrees using 
2067     * the current {@code paint} and {@code stroke}.
2068     * 
2069     * @param x  the x-coordinate.
2070     * @param y  the y-coordinate.
2071     * @param width  the width.
2072     * @param height  the height.
2073     * @param startAngle  the start angle in degrees, 0 = 3 o'clock.
2074     * @param arcAngle  the angle (anticlockwise) in degrees.
2075     * 
2076     * @see #fillArc(int, int, int, int, int, int) 
2077     */
2078    @Override
2079    public void drawArc(int x, int y, int width, int height, int startAngle, 
2080            int arcAngle) {
2081        setArc(x, y, width, height, startAngle, arcAngle);
2082        draw(this.arc);
2083    }
2084
2085    /**
2086     * Fills an arc contained within the rectangle 
2087     * {@code (x, y, width, height)}, starting at {@code startAngle}
2088     * and continuing through {@code arcAngle} degrees, using 
2089     * the current {@code paint}
2090     * 
2091     * @param x  the x-coordinate.
2092     * @param y  the y-coordinate.
2093     * @param width  the width.
2094     * @param height  the height.
2095     * @param startAngle  the start angle in degrees, 0 = 3 o'clock.
2096     * @param arcAngle  the angle (anticlockwise) in degrees.
2097     * 
2098     * @see #drawArc(int, int, int, int, int, int) 
2099     */
2100    @Override
2101    public void fillArc(int x, int y, int width, int height, int startAngle, 
2102            int arcAngle) {
2103        setArc(x, y, width, height, startAngle, arcAngle);
2104        fill(this.arc);
2105    }
2106
2107    /**
2108     * Draws the specified multi-segment line using the current 
2109     * {@code paint} and {@code stroke}.
2110     * 
2111     * @param xPoints  the x-points.
2112     * @param yPoints  the y-points.
2113     * @param nPoints  the number of points to use for the polyline.
2114     */
2115    @Override
2116    public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
2117        GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, 
2118                false);
2119        draw(p);
2120    }
2121
2122    /**
2123     * Draws the specified polygon using the current {@code paint} and 
2124     * {@code stroke}.
2125     * 
2126     * @param xPoints  the x-points.
2127     * @param yPoints  the y-points.
2128     * @param nPoints  the number of points to use for the polygon.
2129     * 
2130     * @see #fillPolygon(int[], int[], int)      */
2131    @Override
2132    public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
2133        GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, 
2134                true);
2135        draw(p);
2136    }
2137
2138    /**
2139     * Fills the specified polygon using the current {@code paint}.
2140     * 
2141     * @param xPoints  the x-points.
2142     * @param yPoints  the y-points.
2143     * @param nPoints  the number of points to use for the polygon.
2144     * 
2145     * @see #drawPolygon(int[], int[], int) 
2146     */
2147    @Override
2148    public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
2149        GeneralPath p = GraphicsUtils.createPolygon(xPoints, yPoints, nPoints, 
2150                true);
2151        fill(p);
2152    }
2153
2154    /**
2155     * Returns the bytes representing a PNG format image.
2156     * 
2157     * @param img  the image to encode.
2158     * 
2159     * @return The bytes representing a PNG format image. 
2160     */
2161    private byte[] getPNGBytes(Image img) {
2162        RenderedImage ri;
2163        if (img instanceof RenderedImage) {
2164            ri = (RenderedImage) img;
2165        } else {
2166            BufferedImage bi = new BufferedImage(img.getWidth(null), 
2167                    img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
2168            Graphics2D g2 = bi.createGraphics();
2169            g2.drawImage(img, 0, 0, null);
2170            ri = bi;
2171        }
2172        ByteArrayOutputStream baos = new ByteArrayOutputStream();
2173        try {
2174            ImageIO.write(ri, "png", baos);
2175        } catch (IOException ex) {
2176            Logger.getLogger(SVGGraphics2D.class.getName()).log(Level.SEVERE, 
2177                    "IOException while writing PNG data.", ex);
2178        }
2179        return baos.toByteArray();
2180    }  
2181    
2182    /**
2183     * Draws an image at the location {@code (x, y)}.  Note that the 
2184     * {@code observer} is ignored.
2185     * 
2186     * @param img  the image ({@code null} not permitted).
2187     * @param x  the x-coordinate.
2188     * @param y  the y-coordinate.
2189     * @param observer  ignored.
2190     * 
2191     * @return {@code true} if the image is drawn. 
2192     */
2193    @Override
2194    public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
2195        int w = img.getWidth(observer);
2196        if (w < 0) {
2197            return false;
2198        }
2199        int h = img.getHeight(observer);
2200        if (h < 0) {
2201            return false;
2202        }
2203        return drawImage(img, x, y, w, h, observer);
2204    }
2205
2206    /**
2207     * Draws the image into the rectangle defined by {@code (x, y, w, h)}.  
2208     * Note that the {@code observer} is ignored (it is not useful in this
2209     * context).
2210     * 
2211     * @param img  the image.
2212     * @param x  the x-coordinate.
2213     * @param y  the y-coordinate.
2214     * @param w  the width.
2215     * @param h  the height.
2216     * @param observer  ignored.
2217     * 
2218     * @return {@code true} if the image is drawn. 
2219     */
2220    @Override
2221    public boolean drawImage(Image img, int x, int y, int w, int h, 
2222            ImageObserver observer) {
2223
2224        // the rendering hints control whether the image is embedded or
2225        // referenced...
2226        Object hint = getRenderingHint(SVGHints.KEY_IMAGE_HANDLING);
2227        if (SVGHints.VALUE_IMAGE_HANDLING_EMBED.equals(hint)) {
2228            this.sb.append("<image ");
2229            appendOptionalElementIDFromHint(this.sb);
2230            this.sb.append("preserveAspectRatio=\"none\" ");
2231            this.sb.append("xlink:href=\"data:image/png;base64,");
2232            this.sb.append(DatatypeConverter.printBase64Binary(getPNGBytes(
2233                    img)));
2234            this.sb.append("\" ");
2235            this.sb.append(getClipPathRef()).append(" ");
2236            this.sb.append("transform=\"").append(getSVGTransform(
2237                    this.transform)).append("\" ");            
2238            this.sb.append("x=\"").append(geomDP(x))
2239                    .append("\" y=\"").append(geomDP(y))
2240                    .append("\" ");
2241            this.sb.append("width=\"").append(geomDP(w)).append("\" height=\"")
2242                    .append(geomDP(h)).append("\"/>\n");
2243            return true;
2244        } else { // here for SVGHints.VALUE_IMAGE_HANDLING_REFERENCE
2245            int count = this.imageElements.size();
2246            String href = (String) this.hints.get(SVGHints.KEY_IMAGE_HREF);
2247            if (href == null) {
2248                href = this.filePrefix + count + this.fileSuffix;
2249            } else {
2250                // KEY_IMAGE_HREF value is for a single use...
2251                this.hints.put(SVGHints.KEY_IMAGE_HREF, null);
2252            }
2253            ImageElement imageElement = new ImageElement(href, img);
2254            this.imageElements.add(imageElement);
2255            // write an SVG element for the img
2256            this.sb.append("<image ");
2257            appendOptionalElementIDFromHint(this.sb);
2258            this.sb.append("xlink:href=\"");
2259            this.sb.append(href).append("\" ");
2260            this.sb.append(getClipPathRef()).append(" ");
2261            this.sb.append("transform=\"").append(getSVGTransform(
2262                    this.transform)).append("\" ");
2263            this.sb.append("x=\"").append(geomDP(x))
2264                    .append("\" y=\"").append(geomDP(y))
2265                    .append("\" ");
2266            this.sb.append("width=\"").append(geomDP(w)).append("\" height=\"")
2267                    .append(geomDP(h)).append("\"/>\n");
2268            return true;
2269        }
2270    }
2271
2272    /**
2273     * Draws an image at the location {@code (x, y)}.  Note that the 
2274     * {@code observer} is ignored.
2275     * 
2276     * @param img  the image ({@code null} not permitted).
2277     * @param x  the x-coordinate.
2278     * @param y  the y-coordinate.
2279     * @param bgcolor  the background color ({@code null} permitted).
2280     * @param observer  ignored.
2281     * 
2282     * @return {@code true} if the image is drawn. 
2283     */
2284    @Override
2285    public boolean drawImage(Image img, int x, int y, Color bgcolor, 
2286            ImageObserver observer) {
2287        int w = img.getWidth(null);
2288        if (w < 0) {
2289            return false;
2290        }
2291        int h = img.getHeight(null);
2292        if (h < 0) {
2293            return false;
2294        }
2295        return drawImage(img, x, y, w, h, bgcolor, observer);
2296    }
2297
2298    /**
2299     * Draws an image to the rectangle {@code (x, y, w, h)} (scaling it if
2300     * required), first filling the background with the specified color.  Note 
2301     * that the {@code observer} is ignored.
2302     * 
2303     * @param img  the image.
2304     * @param x  the x-coordinate.
2305     * @param y  the y-coordinate.
2306     * @param w  the width.
2307     * @param h  the height.
2308     * @param bgcolor  the background color ({@code null} permitted).
2309     * @param observer  ignored.
2310     * 
2311     * @return {@code true} if the image is drawn.      
2312     */
2313    @Override
2314    public boolean drawImage(Image img, int x, int y, int w, int h, 
2315            Color bgcolor, ImageObserver observer) {
2316        Paint saved = getPaint();
2317        setPaint(bgcolor);
2318        fillRect(x, y, w, h);
2319        setPaint(saved);
2320        return drawImage(img, x, y, w, h, observer);
2321    }
2322
2323    /**
2324     * Draws part of an image (defined by the source rectangle 
2325     * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle
2326     * {@code (dx1, dy1, dx2, dy2)}.  Note that the {@code observer} is ignored.
2327     * 
2328     * @param img  the image.
2329     * @param dx1  the x-coordinate for the top left of the destination.
2330     * @param dy1  the y-coordinate for the top left of the destination.
2331     * @param dx2  the x-coordinate for the bottom right of the destination.
2332     * @param dy2  the y-coordinate for the bottom right of the destination.
2333     * @param sx1 the x-coordinate for the top left of the source.
2334     * @param sy1 the y-coordinate for the top left of the source.
2335     * @param sx2 the x-coordinate for the bottom right of the source.
2336     * @param sy2 the y-coordinate for the bottom right of the source.
2337     * 
2338     * @return {@code true} if the image is drawn. 
2339     */
2340    @Override
2341    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 
2342            int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
2343        int w = dx2 - dx1;
2344        int h = dy2 - dy1;
2345        BufferedImage img2 = new BufferedImage(w, h, 
2346                BufferedImage.TYPE_INT_ARGB);
2347        Graphics2D g2 = img2.createGraphics();
2348        g2.drawImage(img, 0, 0, w, h, sx1, sy1, sx2, sy2, null);
2349        return drawImage(img2, dx1, dx2, null);
2350    }
2351
2352    /**
2353     * Draws part of an image (defined by the source rectangle 
2354     * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle
2355     * {@code (dx1, dy1, dx2, dy2)}.  The destination rectangle is first
2356     * cleared by filling it with the specified {@code bgcolor}. Note that
2357     * the {@code observer} is ignored. 
2358     * 
2359     * @param img  the image.
2360     * @param dx1  the x-coordinate for the top left of the destination.
2361     * @param dy1  the y-coordinate for the top left of the destination.
2362     * @param dx2  the x-coordinate for the bottom right of the destination.
2363     * @param dy2  the y-coordinate for the bottom right of the destination.
2364     * @param sx1 the x-coordinate for the top left of the source.
2365     * @param sy1 the y-coordinate for the top left of the source.
2366     * @param sx2 the x-coordinate for the bottom right of the source.
2367     * @param sy2 the y-coordinate for the bottom right of the source.
2368     * @param bgcolor  the background color ({@code null} permitted).
2369     * @param observer  ignored.
2370     * 
2371     * @return {@code true} if the image is drawn. 
2372     */
2373    @Override
2374    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 
2375            int sx1, int sy1, int sx2, int sy2, Color bgcolor, 
2376            ImageObserver observer) {
2377        Paint saved = getPaint();
2378        setPaint(bgcolor);
2379        fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1);
2380        setPaint(saved);
2381        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
2382    }
2383
2384    /**
2385     * Draws the rendered image.
2386     * 
2387     * @param img  the image.
2388     * @param xform  the transform.
2389     */
2390    @Override
2391    public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
2392        BufferedImage bi = GraphicsUtils.convertRenderedImage(img);
2393        drawImage(bi, xform, null);
2394    }
2395
2396    /**
2397     * Draws the renderable image.
2398     * 
2399     * @param img  the renderable image.
2400     * @param xform  the transform.
2401     */
2402    @Override
2403    public void drawRenderableImage(RenderableImage img, 
2404            AffineTransform xform) {
2405        RenderedImage ri = img.createDefaultRendering();
2406        drawRenderedImage(ri, xform);
2407    }
2408
2409    /**
2410     * Draws an image with the specified transform. Note that the 
2411     * {@code observer} is ignored.     
2412     * 
2413     * @param img  the image.
2414     * @param xform  the transform.
2415     * @param obs  the image observer (ignored).
2416     * 
2417     * @return {@code true} if the image is drawn. 
2418     */
2419    @Override
2420    public boolean drawImage(Image img, AffineTransform xform, 
2421            ImageObserver obs) {
2422        AffineTransform savedTransform = getTransform();
2423        transform(xform);
2424        boolean result = drawImage(img, 0, 0, obs);
2425        setTransform(savedTransform);
2426        return result;
2427    }
2428
2429    /**
2430     * Draws the image resulting from applying the {@code BufferedImageOp}
2431     * to the specified image at the location {@code (x, y)}.
2432     * 
2433     * @param img  the image.
2434     * @param op  the operation.
2435     * @param x  the x-coordinate.
2436     * @param y  the y-coordinate.
2437     */
2438    @Override
2439    public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
2440        BufferedImage imageToDraw = op.filter(img, null);
2441        drawImage(imageToDraw, new AffineTransform(1f, 0f, 0f, 1f, x, y), null);
2442    }
2443
2444    /**
2445     * This method does nothing.  The operation assumes that the output is in 
2446     * bitmap form, which is not the case for SVG, so we silently ignore
2447     * this method call.
2448     * 
2449     * @param x  the x-coordinate.
2450     * @param y  the y-coordinate.
2451     * @param width  the width of the area.
2452     * @param height  the height of the area.
2453     * @param dx  the delta x.
2454     * @param dy  the delta y.
2455     */
2456    @Override
2457    public void copyArea(int x, int y, int width, int height, int dx, int dy) {
2458        // do nothing, this operation is silently ignored.
2459    }
2460
2461    /**
2462     * This method does nothing, there are no resources to dispose.
2463     */
2464    @Override
2465    public void dispose() {
2466        // nothing to do
2467    }
2468
2469    /**
2470     * Returns the SVG element that has been generated by calls to this 
2471     * {@code Graphics2D} implementation.
2472     * 
2473     * @return The SVG element.
2474     */
2475    public String getSVGElement() {
2476        return getSVGElement(null);
2477    }
2478    
2479    /**
2480     * Returns the SVG element that has been generated by calls to this
2481     * {@code Graphics2D} implementation, giving it the specified {@code id}.  
2482     * If {@code id} is {@code null}, the element will have no {@code id} 
2483     * attribute.
2484     * 
2485     * @param id  the element id ({@code null} permitted).
2486     * 
2487     * @return A string containing the SVG element. 
2488     * 
2489     * @since 1.8
2490     */
2491    public String getSVGElement(String id) {
2492        StringBuilder svg = new StringBuilder("<svg ");
2493        if (id != null) {
2494            svg.append("id=\"").append(id).append("\" ");
2495        }
2496        svg.append("xmlns=\"http://www.w3.org/2000/svg\" ")
2497           .append("xmlns:xlink=\"http://www.w3.org/1999/xlink\" ")
2498           .append("xmlns:jfreesvg=\"http://www.jfree.org/jfreesvg/svg\" ")
2499           .append("width=\"").append(this.width)
2500           .append("\" height=\"").append(this.height)
2501           .append("\" text-rendering=\"").append(this.textRendering)
2502           .append("\" shape-rendering=\"").append(this.shapeRendering)
2503           .append("\">\n");
2504        StringBuilder defs = new StringBuilder("<defs>");
2505        for (GradientPaintKey key : this.gradientPaints.keySet()) {
2506            defs.append(getLinearGradientElement(this.gradientPaints.get(key), 
2507                    key.getPaint()));
2508            defs.append("\n");
2509        }
2510        for (LinearGradientPaintKey key : this.linearGradientPaints.keySet()) {
2511            defs.append(getLinearGradientElement(
2512                    this.linearGradientPaints.get(key), key.getPaint()));
2513            defs.append("\n");            
2514        }
2515        for (RadialGradientPaintKey key : this.radialGradientPaints.keySet()) {
2516            defs.append(getRadialGradientElement(
2517                    this.radialGradientPaints.get(key), key.getPaint()));
2518            defs.append("\n");
2519        }
2520        for (int i = 0; i < this.clipPaths.size(); i++) {
2521            StringBuilder b = new StringBuilder("<clipPath id=\"")
2522                    .append(this.defsKeyPrefix).append(CLIP_KEY_PREFIX).append(i)
2523                    .append("\">");
2524            b.append("<path ").append(this.clipPaths.get(i)).append("/>");
2525            b.append("</clipPath>").append("\n");
2526            defs.append(b.toString());
2527        }
2528        defs.append("</defs>\n");
2529        svg.append(defs);
2530        svg.append(this.sb);
2531        svg.append("</svg>");        
2532        return svg.toString();
2533    }
2534    
2535    /**
2536     * Returns an SVG document.
2537     * 
2538     * @return An SVG document.
2539     */
2540    public String getSVGDocument() {
2541        StringBuilder b = new StringBuilder();
2542        b.append("<?xml version=\"1.0\"?>\n");
2543        b.append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" ");
2544        b.append("\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n");
2545        b.append(getSVGElement());
2546        return b.append("\n").toString();
2547    }
2548    
2549    /**
2550     * Returns the list of image elements that have been referenced in the 
2551     * SVG output but not embedded.  If the image files don't already exist,
2552     * you can use this list as the basis for creating the image files.
2553     * 
2554     * @return The list of image elements.
2555     * 
2556     * @see SVGHints#KEY_IMAGE_HANDLING
2557     */
2558    public List<ImageElement> getSVGImages() {
2559        return this.imageElements;
2560    }
2561    
2562    /**
2563     * Returns a new set containing the element IDs that have been used in
2564     * output so far.
2565     * 
2566     * @return The element IDs.
2567     * 
2568     * @since 1.5
2569     */
2570    public Set<String> getElementIDs() {
2571        return new HashSet<String>(this.elementIDs);
2572    }
2573    
2574    /**
2575     * Returns an element to represent a linear gradient.  All the linear
2576     * gradients that are used get written to the DEFS element in the SVG.
2577     * 
2578     * @param id  the reference id.
2579     * @param paint  the gradient.
2580     * 
2581     * @return The SVG element.
2582     */
2583    private String getLinearGradientElement(String id, GradientPaint paint) {
2584        StringBuilder b = new StringBuilder("<linearGradient id=\"").append(id)
2585                .append("\" ");
2586        Point2D p1 = paint.getPoint1();
2587        Point2D p2 = paint.getPoint2();
2588        b.append("x1=\"").append(geomDP(p1.getX())).append("\" ");
2589        b.append("y1=\"").append(geomDP(p1.getY())).append("\" ");
2590        b.append("x2=\"").append(geomDP(p2.getX())).append("\" ");
2591        b.append("y2=\"").append(geomDP(p2.getY())).append("\" ");
2592        b.append("gradientUnits=\"userSpaceOnUse\">");
2593        b.append("<stop offset=\"0%\" style=\"stop-color: ").append(
2594                rgbColorStr(paint.getColor1())).append(";\"/>");
2595        b.append("<stop offset=\"100%\" style=\"stop-color: ").append(
2596                rgbColorStr(paint.getColor2())).append(";\"/>");
2597        return b.append("</linearGradient>").toString();
2598    }
2599    
2600    /**
2601     * Returns an element to represent a linear gradient.  All the linear
2602     * gradients that are used get written to the DEFS element in the SVG.
2603     * 
2604     * @param id  the reference id.
2605     * @param paint  the gradient.
2606     * 
2607     * @return The SVG element.
2608     */
2609    private String getLinearGradientElement(String id, LinearGradientPaint paint) {
2610        StringBuilder b = new StringBuilder("<linearGradient id=\"").append(id)
2611                .append("\" ");
2612        Point2D p1 = paint.getStartPoint();
2613        Point2D p2 = paint.getEndPoint();
2614        b.append("x1=\"").append(geomDP(p1.getX())).append("\" ");
2615        b.append("y1=\"").append(geomDP(p1.getY())).append("\" ");
2616        b.append("x2=\"").append(geomDP(p2.getX())).append("\" ");
2617        b.append("y2=\"").append(geomDP(p2.getY())).append("\" ");
2618        if (!paint.getCycleMethod().equals(CycleMethod.NO_CYCLE)) {
2619            String sm = paint.getCycleMethod().equals(CycleMethod.REFLECT) 
2620                    ? "reflect" : "repeat";
2621            b.append("spreadMethod=\"").append(sm).append("\" ");
2622        }
2623        b.append("gradientUnits=\"userSpaceOnUse\">");
2624        for (int i = 0; i < paint.getFractions().length; i++) {
2625            Color c = paint.getColors()[i];
2626            float fraction = paint.getFractions()[i];
2627            b.append("<stop offset=\"").append(geomDP(fraction * 100))
2628                    .append("%\" style=\"stop-color: ")
2629                    .append(rgbColorStr(c)).append(";\"/>");
2630        }
2631        return b.append("</linearGradient>").toString();
2632    }
2633    
2634    /**
2635     * Returns an element to represent a radial gradient.  All the radial
2636     * gradients that are used get written to the DEFS element in the SVG.
2637     * 
2638     * @param id  the reference id.
2639     * @param rgp  the radial gradient.
2640     * 
2641     * @return The SVG element. 
2642     */
2643    private String getRadialGradientElement(String id, RadialGradientPaint rgp) {
2644        StringBuilder b = new StringBuilder("<radialGradient id=\"").append(id)
2645                .append("\" gradientUnits=\"userSpaceOnUse\" ");
2646        Point2D center = rgp.getCenterPoint();
2647        Point2D focus = rgp.getFocusPoint();
2648        float radius = rgp.getRadius();
2649        b.append("cx=\"").append(geomDP(center.getX())).append("\" ");
2650        b.append("cy=\"").append(geomDP(center.getY())).append("\" ");
2651        b.append("r=\"").append(geomDP(radius)).append("\" ");
2652        b.append("fx=\"").append(geomDP(focus.getX())).append("\" ");
2653        b.append("fy=\"").append(geomDP(focus.getY())).append("\">");
2654        
2655        Color[] colors = rgp.getColors();
2656        float[] fractions = rgp.getFractions();
2657        for (int i = 0; i < colors.length; i++) {
2658            Color c = colors[i];
2659            float f = fractions[i];
2660            b.append("<stop offset=\"").append(geomDP(f * 100)).append("%\" ");
2661            b.append("stop-color=\"").append(rgbColorStr(c)).append("\"/>");
2662        }
2663        return b.append("</radialGradient>").toString();
2664    }
2665
2666    /**
2667     * Returns a clip path reference for the current user clip.  This is 
2668     * written out on all SVG elements that draw or fill shapes or text.
2669     * 
2670     * @return A clip path reference. 
2671     */
2672    private String getClipPathRef() {
2673        if (this.clip == null) {
2674            return "";
2675        }
2676        if (this.clipRef == null) {
2677            this.clipRef = registerClip(getClip());
2678        }
2679        StringBuilder b = new StringBuilder();
2680        b.append("clip-path=\"url(#").append(this.clipRef).append(")\"");
2681        return b.toString();
2682    }
2683    
2684    /**
2685     * Sets the attributes of the reusable {@link Rectangle2D} object that is
2686     * used by the {@link SVGGraphics2D#drawRect(int, int, int, int)} and 
2687     * {@link SVGGraphics2D#fillRect(int, int, int, int)} methods.
2688     * 
2689     * @param x  the x-coordinate.
2690     * @param y  the y-coordinate.
2691     * @param width  the width.
2692     * @param height  the height.
2693     */
2694    private void setRect(int x, int y, int width, int height) {
2695        if (this.rect == null) {
2696            this.rect = new Rectangle2D.Double(x, y, width, height);
2697        } else {
2698            this.rect.setRect(x, y, width, height);
2699        }
2700    }
2701    
2702    /**
2703     * Sets the attributes of the reusable {@link RoundRectangle2D} object that
2704     * is used by the {@link #drawRoundRect(int, int, int, int, int, int)} and
2705     * {@link #fillRoundRect(int, int, int, int, int, int)} methods.
2706     * 
2707     * @param x  the x-coordinate.
2708     * @param y  the y-coordinate.
2709     * @param width  the width.
2710     * @param height  the height.
2711     * @param arcWidth  the arc width.
2712     * @param arcHeight  the arc height.
2713     */
2714    private void setRoundRect(int x, int y, int width, int height, int arcWidth, 
2715            int arcHeight) {
2716        if (this.roundRect == null) {
2717            this.roundRect = new RoundRectangle2D.Double(x, y, width, height, 
2718                    arcWidth, arcHeight);
2719        } else {
2720            this.roundRect.setRoundRect(x, y, width, height, 
2721                    arcWidth, arcHeight);
2722        }        
2723    }
2724
2725    /**
2726     * Sets the attributes of the reusable {@link Arc2D} object that is used by
2727     * {@link #drawArc(int, int, int, int, int, int)} and 
2728     * {@link #fillArc(int, int, int, int, int, int)} methods.
2729     * 
2730     * @param x  the x-coordinate.
2731     * @param y  the y-coordinate.
2732     * @param width  the width.
2733     * @param height  the height.
2734     * @param startAngle  the start angle in degrees, 0 = 3 o'clock.
2735     * @param arcAngle  the angle (anticlockwise) in degrees.
2736     */
2737    private void setArc(int x, int y, int width, int height, int startAngle, 
2738            int arcAngle) {
2739        if (this.arc == null) {
2740            this.arc = new Arc2D.Double(x, y, width, height, startAngle, 
2741                    arcAngle, Arc2D.OPEN);
2742        } else {
2743            this.arc.setArc(x, y, width, height, startAngle, arcAngle, 
2744                    Arc2D.OPEN);
2745        }        
2746    }
2747    
2748    /**
2749     * Sets the attributes of the reusable {@link Ellipse2D} object that is 
2750     * used by the {@link #drawOval(int, int, int, int)} and
2751     * {@link #fillOval(int, int, int, int)} methods.
2752     * 
2753     * @param x  the x-coordinate.
2754     * @param y  the y-coordinate.
2755     * @param width  the width.
2756     * @param height  the height.
2757     */
2758    private void setOval(int x, int y, int width, int height) {
2759        if (this.oval == null) {
2760            this.oval = new Ellipse2D.Double(x, y, width, height);
2761        } else {
2762            this.oval.setFrame(x, y, width, height);
2763        }
2764    }
2765
2766}