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