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