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