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