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