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