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