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