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