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