001/* ============
002 * FXGraphics2D
003 * ============
004 * 
005 * (C)opyright 2014-2022, by David Gilbert.
006 * 
007 * https://github.com/jfree/fxgraphics2d
008 *
009 * The FXGraphics2D class has been developed by David Gilbert for
010 * use in Orson Charts (https://github.com/jfree/orson-charts) and
011 * JFreeChart (http://www.jfree.org/jfreechart).  It may be useful for other
012 * code that uses the Graphics2D API provided by Java2D.
013 * 
014 * Redistribution and use in source and binary forms, with or without
015 * modification, are permitted provided that the following conditions are met:
016 *   - Redistributions of source code must retain the above copyright
017 *     notice, this list of conditions and the following disclaimer.
018 *   - Redistributions in binary form must reproduce the above copyright
019 *     notice, this list of conditions and the following disclaimer in the
020 *     documentation and/or other materials provided with the distribution.
021 *   - Neither the name of JFree.org nor the names of its contributors may
022 *     be used to endorse or promote products derived from this software
023 *     without specific prior written permission.
024 *
025 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
026 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
027 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
028 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
029 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
030 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
031 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
032 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
033 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
034 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
035 * THE POSSIBILITY OF SUCH DAMAGE.
036 * 
037 */
038
039package org.jfree.fx;
040
041import java.awt.AlphaComposite;
042import java.awt.BasicStroke;
043import java.awt.Color;
044import java.awt.Composite;
045import java.awt.Font;
046import java.awt.FontMetrics;
047import java.awt.GradientPaint;
048import java.awt.Graphics;
049import java.awt.Graphics2D;
050import java.awt.GraphicsConfiguration;
051import java.awt.Image;
052import java.awt.LinearGradientPaint;
053import java.awt.MultipleGradientPaint;
054import java.awt.Paint;
055import java.awt.RadialGradientPaint;
056import java.awt.Rectangle;
057import java.awt.RenderingHints;
058import java.awt.Shape;
059import java.awt.Stroke;
060import java.awt.font.FontRenderContext;
061import java.awt.font.GlyphVector;
062import java.awt.font.TextLayout;
063import java.awt.geom.AffineTransform;
064import java.awt.geom.Arc2D;
065import java.awt.geom.Area;
066import java.awt.geom.Ellipse2D;
067import java.awt.geom.GeneralPath;
068import java.awt.geom.Line2D;
069import java.awt.geom.NoninvertibleTransformException;
070import java.awt.geom.Path2D;
071import java.awt.geom.PathIterator;
072import java.awt.geom.Point2D;
073import java.awt.geom.Rectangle2D;
074import java.awt.geom.RoundRectangle2D;
075import java.awt.image.BufferedImage;
076import java.awt.image.BufferedImageOp;
077import java.awt.image.ColorModel;
078import java.awt.image.ImageObserver;
079import java.awt.image.RenderedImage;
080import java.awt.image.WritableRaster;
081import java.awt.image.renderable.RenderableImage;
082import java.text.AttributedCharacterIterator;
083import java.util.Arrays;
084import java.util.Hashtable;
085import java.util.Map;
086import java.util.Set;
087import javafx.embed.swing.SwingFXUtils;
088
089import javafx.scene.canvas.Canvas;
090import javafx.scene.canvas.GraphicsContext;
091import javafx.scene.effect.BlendMode;
092import javafx.scene.paint.CycleMethod;
093import javafx.scene.paint.LinearGradient;
094import javafx.scene.paint.RadialGradient;
095import javafx.scene.paint.Stop;
096import javafx.scene.shape.ArcType;
097import javafx.scene.shape.FillRule;
098import javafx.scene.shape.StrokeLineCap;
099import javafx.scene.shape.StrokeLineJoin;
100import javafx.scene.text.FontPosture;
101import javafx.scene.text.FontWeight;
102
103/**
104 * A {@link Graphics2D} implementation that writes to a JavaFX {@link Canvas}.
105 * This is intended for general purpose usage, but has been created for use in
106 * Orson Charts (<a href="https://github.com/jfree/orson-charts/">https://github.com/jfree/orson-charts/</a>) and
107 * JFreeChart (<a href="http://www.jfree.org/jfreechart/">http://www.jfree.org/jfreechart/</a>).
108 */
109public class FXGraphics2D extends Graphics2D {
110    
111    /** The graphics context for the JavaFX canvas. */
112    private final GraphicsContext gc;
113    
114    /** Rendering hints. */
115    private final RenderingHints hints;
116    
117    private Shape clip;
118    
119    /** Stores the AWT Paint object for get/setPaint(). */
120    private Paint paint = Color.BLACK;
121    
122    /** Stores the AWT Color object for get/setColor(). */
123    private Color color = Color.BLACK;
124    
125    private Composite composite = AlphaComposite.getInstance(
126            AlphaComposite.SRC_OVER, 1.0f);
127
128    private Stroke stroke = new BasicStroke(1.0f);
129 
130    /** 
131     * The width of the stroke to use when the user supplies a
132     * BasicStroke with a width of 0.0 (in this case the Java specification
133     * says "If width is set to 0.0f, the stroke is rendered as the thinnest 
134     * possible line for the target device and the antialias hint setting.")
135     */
136    private double zeroStrokeWidth;
137    
138    private Font font = new Font("SansSerif", Font.PLAIN, 12);
139    
140    private final FontRenderContext fontRenderContext = new FontRenderContext(
141            null, false, true);
142
143    private AffineTransform transform = new AffineTransform();
144
145    /** The background color, used in the {@code clearRect()} method. */
146    private Color background = Color.BLACK;
147    
148    /** A flag that is set when the JavaFX graphics state has been saved. */
149    private boolean stateSaved = false;
150
151    private Stroke savedStroke;
152    private Paint savedPaint;
153    private Color savedColor;
154    private Font savedFont;
155    private AffineTransform savedTransform;
156    
157    /**
158     * An instance that is lazily instantiated in drawLine and then 
159     * subsequently reused to avoid creating a lot of garbage.
160     */
161    private Line2D line;
162    
163    /**
164     * An instance that is lazily instantiated in fillRect and then 
165     * subsequently reused to avoid creating a lot of garbage.
166     */
167    Rectangle2D rect;
168
169    /**
170     * An instance that is lazily instantiated in draw/fillRoundRect and then
171     * subsequently reused to avoid creating a lot of garbage.
172     */
173    private RoundRectangle2D roundRect;
174    
175     /**
176     * An instance that is lazily instantiated in draw/fillOval and then
177     * subsequently reused to avoid creating a lot of garbage.
178     */
179   private Ellipse2D oval;
180    
181    /**
182     * An instance that is lazily instantiated in draw/fillArc and then
183     * subsequently reused to avoid creating a lot of garbage.
184     */
185    private Arc2D arc;
186    
187    /** A hidden image used for font metrics. */
188    private BufferedImage fmImage;
189
190    /** 
191     * A Graphics2D instance for the hidden image that is used for font
192     * metrics.  Used in the getFontMetrics(Font f) method.
193     */
194    private Graphics2D fmImageG2;
195    
196    /** The FXFontMetrics. */
197    private FXFontMetrics fxFontMetrics;
198
199    /** 
200     * The device configuration (this is lazily instantiated in the 
201     * getDeviceConfiguration() method).
202     */
203    private GraphicsConfiguration deviceConfiguration;
204    
205    /**
206     * Throws an {@code IllegalArgumentException} if {@code arg} is
207     * {@code null}.
208     * 
209     * @param arg  the argument to check.
210     * @param name  the name of the argument (to display in the exception 
211     *         message).
212     */
213    private static void nullNotPermitted(Object arg, String name) {
214        if (arg == null) {
215            throw new IllegalArgumentException("Null '" + name + "' argument.");
216        }    
217    }
218    
219    /**
220     * Creates a new instance that will render to the specified JavaFX
221     * {@code GraphicsContext}.
222     * 
223     * @param gc  the graphics context ({@code null} not permitted). 
224     */
225    public FXGraphics2D(GraphicsContext gc) {
226        nullNotPermitted(gc, "gc");
227        this.gc = gc;
228        this.zeroStrokeWidth = 0.5;
229        this.hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, 
230                RenderingHints.VALUE_ANTIALIAS_DEFAULT);
231        this.hints.put(FXHints.KEY_USE_FX_FONT_METRICS, true);
232    }
233    
234    /**
235     * Returns the width to use for the stroke when the AWT stroke
236     * specified has a zero width (the default value is {@code 0.5}).  
237     * <p>In the Java specification for {@code BasicStroke} it states "If width 
238     * is set to 0.0f, the stroke is rendered as the thinnest possible 
239     * line for the target device and the antialias hint setting."  We don't 
240     * have a means to implement that accurately since we must specify a fixed
241     * width to the JavaFX canvas - this attribute is the width that is 
242     * used.</p>
243     * 
244     * @return The width.
245     */
246    public double getZeroStrokeWidth() {
247        return this.zeroStrokeWidth;
248    }
249    
250    /**
251     * Sets the width to use for the stroke when setting a new AWT stroke that
252     * has a width of {@code 0.0}.
253     * 
254     * @param width  the new width (must be 0.0 or greater).
255     */
256    public void setZeroStrokeWidth(double width) {
257        if (width < 0.0) {
258            throw new IllegalArgumentException("Width cannot be negative.");
259        }
260        this.zeroStrokeWidth = width;
261    }
262    
263    /**
264     * Returns the device configuration associated with this
265     * {@code Graphics2D}.
266     * 
267     * @return The device configuration (never {@code null}).
268     */
269    @Override
270    public GraphicsConfiguration getDeviceConfiguration() {
271        if (this.deviceConfiguration == null) {
272            int width = (int) this.gc.getCanvas().getWidth();
273            int height = (int) this.gc.getCanvas().getHeight();
274            this.deviceConfiguration = new FXGraphicsConfiguration(width,
275                    height);
276        }
277        return this.deviceConfiguration;
278    }
279
280    /**
281     * Creates a new graphics object that is a copy of this graphics object.
282     * 
283     * @return A new graphics object.
284     */
285    @Override
286    public Graphics create() {
287        FXGraphics2D copy = new FXGraphics2D(this.gc);
288        copy.setRenderingHints(getRenderingHints());
289        copy.setClip(getClip());
290        copy.setPaint(getPaint());
291        copy.setColor(getColor());
292        copy.setComposite(getComposite());
293        copy.setStroke(getStroke());
294        copy.setFont(getFont());
295        copy.setTransform(getTransform());
296        copy.setBackground(getBackground());    
297        return copy;
298    }
299
300    /**
301     * Returns the paint used to draw or fill shapes (or text).  The default 
302     * value is {@link Color#BLACK}.  This attribute is updated by both the
303     * {@link #setPaint(java.awt.Paint)} and {@link #setColor(java.awt.Color)}
304     * methods.
305     * 
306     * @return The paint (never {@code null}). 
307     * 
308     * @see #setPaint(java.awt.Paint) 
309     */
310    @Override
311    public Paint getPaint() {
312        return this.paint;
313    }
314
315    /**
316     * Sets the paint used to draw or fill shapes (or text).  If 
317     * {@code paint} is an instance of {@code Color}, this method will
318     * also update the current color attribute (see {@link #getColor()}). If 
319     * you pass {@code null} to this method, it does nothing (in 
320     * accordance with the JDK specification).
321     * <br><br>
322     * Note that this implementation will map {@link Color}, 
323     * {@link GradientPaint}, {@link LinearGradientPaint} and 
324     * {@link RadialGradientPaint} to JavaFX equivalents, other paint 
325     * implementations are not handled.
326     * 
327     * @param paint  the paint ({@code null} is permitted but ignored).
328     * 
329     * @see #getPaint() 
330     */
331    @Override
332    public void setPaint(Paint paint) {
333        if (paint == null) {
334            return;
335        }
336        if (paintsAreEqual(paint, this.paint)) {
337            return;
338        }
339        applyPaint(paint);
340    }
341
342    private CycleMethod toJavaFXCycleMethod(MultipleGradientPaint.CycleMethod method) {
343        switch (method) {
344            case NO_CYCLE:
345                return CycleMethod.NO_CYCLE;
346            case REFLECT:
347                return CycleMethod.REFLECT;
348            case REPEAT:
349                return CycleMethod.REPEAT;
350            default:
351                throw new IllegalStateException("Unknown cycle method " + method);
352        }
353    }
354
355    private void applyPaint(Paint paint) {
356        this.paint = paint;
357        if (paint instanceof Color) {
358            setColor((Color) paint);
359        } else if (paint instanceof GradientPaint) {
360            GradientPaint gp = (GradientPaint) paint;
361            Stop[] stops = new Stop[] { new Stop(0, 
362                    awtColorToJavaFX(gp.getColor1())), 
363                    new Stop(1, awtColorToJavaFX(gp.getColor2())) };
364            Point2D p1 = gp.getPoint1();
365            Point2D p2 = gp.getPoint2();
366            CycleMethod cm = gp.isCyclic() ? CycleMethod.REFLECT : CycleMethod.NO_CYCLE;
367            LinearGradient lg = new LinearGradient(p1.getX(), p1.getY(), 
368                    p2.getX(), p2.getY(), false, cm, stops);
369            this.gc.setStroke(lg);
370            this.gc.setFill(lg);
371        } else if (paint instanceof MultipleGradientPaint) {
372            MultipleGradientPaint mgp = (MultipleGradientPaint) paint;
373            Color[] colors = mgp.getColors();
374            float[] fractions = mgp.getFractions();
375            Stop[] stops = new Stop[colors.length];
376            for (int i = 0; i < colors.length; i++) {
377                stops[i] = new Stop(fractions[i], awtColorToJavaFX(colors[i]));
378            }
379
380            if (paint instanceof RadialGradientPaint) {
381                RadialGradientPaint rgp = (RadialGradientPaint) paint;
382                Point2D center = rgp.getCenterPoint();
383                Point2D focus = rgp.getFocusPoint();           
384                double focusDistance = focus.distance(center);
385                double focusAngle = 0.0;
386                if (!focus.equals(center)) {
387                    focusAngle = Math.atan2(focus.getY() - center.getY(), 
388                        focus.getX() - center.getX());
389                }
390                double radius = rgp.getRadius();
391                RadialGradient rg = new RadialGradient(
392                        Math.toDegrees(focusAngle), focusDistance / radius,
393                        center.getX(), center.getY(), radius, false, 
394                        toJavaFXCycleMethod(rgp.getCycleMethod()), stops);
395                this.gc.setStroke(rg);
396                this.gc.setFill(rg);
397            } else if (paint instanceof LinearGradientPaint) {
398                LinearGradientPaint lgp = (LinearGradientPaint) paint;
399                Point2D start = lgp.getStartPoint();
400                Point2D end = lgp.getEndPoint();
401                LinearGradient lg = new LinearGradient(start.getX(), 
402                        start.getY(), end.getX(), end.getY(), false, 
403                        toJavaFXCycleMethod(lgp.getCycleMethod()), stops);
404                this.gc.setStroke(lg);
405                this.gc.setFill(lg);
406            }
407        } else {
408            // this is a paint we don't recognise
409        }
410    }
411
412    /**
413     * Returns the foreground color.  This method exists for backwards
414     * compatibility in AWT, you should use the {@link #getPaint()} method.
415     * This attribute is updated by the {@link #setColor(java.awt.Color)}
416     * method, and also by the {@link #setPaint(java.awt.Paint)} method if
417     * a {@code Color} instance is passed to the method.
418     * 
419     * @return The foreground color (never {@code null}).
420     * 
421     * @see #getPaint() 
422     */
423    @Override
424    public Color getColor() {
425        return this.color;
426    }
427
428    /**
429     * Sets the foreground color.  This method exists for backwards 
430     * compatibility in AWT, you should use the 
431     * {@link #setPaint(java.awt.Paint)} method.
432     * 
433     * @param c  the color ({@code null} permitted but ignored). 
434     * 
435     * @see #setPaint(java.awt.Paint) 
436     */
437    @Override
438    public void setColor(Color c) {
439        if (c == null || c.equals(this.color)) {
440            return;
441        }
442        applyColor(c);
443    }
444    
445    private void applyColor(Color c) {
446        this.color = c;
447        this.paint = c;
448        javafx.scene.paint.Color fxcolor = awtColorToJavaFX(c);
449        this.gc.setFill(fxcolor);
450        this.gc.setStroke(fxcolor);
451    }
452
453    /**
454     * Returns a JavaFX color that is equivalent to the specified AWT color.
455     * 
456     * @param c  the color ({@code null} not permitted).
457     * 
458     * @return A JavaFX color. 
459     */
460    private javafx.scene.paint.Color awtColorToJavaFX(Color c) {
461        return javafx.scene.paint.Color.rgb(c.getRed(), c.getGreen(), 
462                c.getBlue(), c.getAlpha() / 255.0);
463    }
464    
465    /**
466     * Returns the background color (the default value is {@link Color#BLACK}).
467     * This attribute is used by the {@link #clearRect(int, int, int, int)} 
468     * method.
469     * 
470     * @return The background color (possibly {@code null}). 
471     * 
472     * @see #setBackground(java.awt.Color) 
473     */
474    @Override
475    public Color getBackground() {
476        return this.background;
477    }
478
479    /**
480     * Sets the background color.  This attribute is used by the 
481     * {@link #clearRect(int, int, int, int)} method.  The reference 
482     * implementation allows {@code null} for the background color, so
483     * we allow that too (but for that case, the {@link #clearRect(int, int, int, int)} 
484     * method will do nothing).
485     * 
486     * @param color  the color ({@code null} permitted).
487     * 
488     * @see #getBackground() 
489     */
490    @Override
491    public void setBackground(Color color) {
492        this.background = color;
493    }
494
495    /**
496     * Returns the current composite.
497     * 
498     * @return The current composite (never {@code null}).
499     * 
500     * @see #setComposite(java.awt.Composite) 
501     */
502    @Override
503    public Composite getComposite() {
504        return this.composite;
505    }
506    
507    /**
508     * Sets the composite.  There is limited handling for 
509     * {@code AlphaComposite}, other composites will have no effect on the 
510     * output.
511     * 
512     * @param comp  the composite ({@code null} not permitted).
513     * 
514     * @see #getComposite() 
515     */
516    @Override
517    public void setComposite(Composite comp) {
518        nullNotPermitted(comp, "comp");
519        this.composite = comp;
520        if (comp instanceof AlphaComposite) {
521            AlphaComposite ac = (AlphaComposite) comp;
522            this.gc.setGlobalAlpha(ac.getAlpha());
523            this.gc.setGlobalBlendMode(blendMode(ac.getRule()));
524        }
525    }
526    
527    /**
528     * Returns a JavaFX BlendMode that is the closest match for the Java2D 
529     * alpha composite rule.
530     * 
531     * @param rule  the rule.
532     * 
533     * @return The blend mode. 
534     */
535    private BlendMode blendMode(int rule) {
536        switch (rule) {
537            case AlphaComposite.SRC_ATOP:
538                return BlendMode.SRC_ATOP;
539            case AlphaComposite.CLEAR:
540            case AlphaComposite.DST:
541            case AlphaComposite.DST_ATOP:
542            case AlphaComposite.DST_IN:
543            case AlphaComposite.DST_OUT:
544            case AlphaComposite.DST_OVER:
545            case AlphaComposite.SRC:
546            case AlphaComposite.SRC_IN:
547            case AlphaComposite.SRC_OUT:
548            case AlphaComposite.SRC_OVER:
549            case AlphaComposite.XOR:
550                return BlendMode.SRC_OVER;
551            default:
552                return BlendMode.SRC_OVER;
553        }
554    }
555
556    /**
557     * Returns the current stroke (this attribute is used when drawing shapes). 
558     * 
559     * @return The current stroke (never {@code null}). 
560     * 
561     * @see #setStroke(java.awt.Stroke) 
562     */
563    @Override
564    public Stroke getStroke() {
565        return this.stroke;
566    }
567
568    /**
569     * Sets the stroke that will be used to draw shapes.  
570     * 
571     * @param s  the stroke ({@code null} not permitted).
572     * 
573     * @see #getStroke() 
574     */
575    @Override
576    public void setStroke(Stroke s) {
577        nullNotPermitted(s, "s");
578        if (s == this.stroke) { // quick test, full equals test later
579            return;
580        }
581        if (stroke instanceof BasicStroke) {
582            BasicStroke bs = (BasicStroke) s;
583            if (bs.equals(this.stroke)) {
584                return; // no change
585            }
586        }
587        this.stroke = s;
588        applyStroke(s);
589    }
590    
591    private void applyStroke(Stroke s) {
592        if (s instanceof BasicStroke) {
593            applyBasicStroke((BasicStroke) s);
594        }
595    }
596    
597    private void applyBasicStroke(BasicStroke bs) {
598        double lineWidth = bs.getLineWidth();
599        if (lineWidth == 0.0) {
600            lineWidth = this.zeroStrokeWidth;
601        }
602        this.gc.setLineWidth(lineWidth);
603        this.gc.setLineCap(awtToJavaFXLineCap(bs.getEndCap()));
604        this.gc.setLineJoin(awtToJavaFXLineJoin(bs.getLineJoin()));
605        this.gc.setMiterLimit(bs.getMiterLimit());
606        this.gc.setLineDashes(floatToDoubleArray(bs.getDashArray()));
607        this.gc.setLineDashOffset(bs.getDashPhase());   
608    }
609    
610    /**
611     * Maps a line cap code from AWT to the corresponding JavaFX StrokeLineCap
612     * enum value.
613     * 
614     * @param c  the line cap code.
615     * 
616     * @return A JavaFX line cap value. 
617     */
618    private StrokeLineCap awtToJavaFXLineCap(int c) {
619        if (c == BasicStroke.CAP_BUTT) {
620            return StrokeLineCap.BUTT;
621        } else if (c == BasicStroke.CAP_ROUND) {
622            return StrokeLineCap.ROUND;
623        } else if (c == BasicStroke.CAP_SQUARE) {
624            return StrokeLineCap.SQUARE;
625        } else {
626            throw new IllegalArgumentException("Unrecognised cap code: " + c);
627        }
628    }
629
630    /**
631     * Maps a line join code from AWT to the corresponding JavaFX 
632     * StrokeLineJoin enum value.
633     * 
634     * @param j  the line join code.
635     * 
636     * @return A JavaFX line join value. 
637     */
638    private StrokeLineJoin awtToJavaFXLineJoin(int j) {
639        if (j == BasicStroke.JOIN_BEVEL) {
640            return StrokeLineJoin.BEVEL;
641        } else if (j == BasicStroke.JOIN_MITER) {
642            return StrokeLineJoin.MITER;
643        } else if (j == BasicStroke.JOIN_ROUND) {
644            return StrokeLineJoin.ROUND;
645        } else {
646            throw new IllegalArgumentException("Unrecognised join code: " + j);            
647        }
648    }
649    
650    private double[] floatToDoubleArray(float[] f) {
651        if (f == null) {
652            return null;
653        }
654        double[] d = new double[f.length];
655        for (int i = 0; i < f.length; i++) {
656            d[i] = f[i];
657        }
658        return d;
659    }
660
661    /**
662     * Returns the current value for the specified hint.
663     * 
664     * @param hintKey  the hint key ({@code null} permitted, but the
665     *     result will be {@code null} also in that case).
666     * 
667     * @return The current value for the specified hint 
668     *     (possibly {@code null}).
669     * 
670     * @see #setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object) 
671     */
672    @Override
673    public Object getRenderingHint(RenderingHints.Key hintKey) {
674        return this.hints.get(hintKey);
675    }
676
677    /**
678     * Sets the value for a hint.  See the {@link FXHints} class for 
679     * information about the hints that can be used with this implementation.
680     * 
681     * @param hintKey  the hint key ({@code null} not permitted).
682     * @param hintValue  the hint value.
683     * 
684     * @see #getRenderingHint(java.awt.RenderingHints.Key) 
685     */
686    @Override
687    public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
688        this.hints.put(hintKey, hintValue);
689    }
690
691    /**
692     * Returns a copy of the rendering hints.  Modifying the returned copy
693     * will have no impact on the state of this {@code Graphics2D} 
694     * instance.
695     * 
696     * @return The rendering hints (never {@code null}). 
697     * 
698     * @see #setRenderingHints(java.util.Map) 
699     */
700    @Override
701    public RenderingHints getRenderingHints() {
702        return (RenderingHints) this.hints.clone();
703    }
704
705    /**
706     * Sets the rendering hints to the specified collection.
707     * 
708     * @param hints  the new set of hints ({@code null} not permitted).
709     * 
710     * @see #getRenderingHints() 
711     */
712    @Override
713    public void setRenderingHints(Map<?, ?> hints) {
714        this.hints.clear();
715        this.hints.putAll(hints);
716    }
717
718    /**
719     * Adds all the supplied rendering hints.
720     * 
721     * @param hints  the hints ({@code null} not permitted).
722     */
723    @Override
724    public void addRenderingHints(Map<?, ?> hints) {
725        this.hints.putAll(hints);
726    }
727
728    /**
729     * Draws the specified shape with the current {@code paint} and 
730     * {@code stroke}.  There is direct handling for {@code Line2D}, 
731     * {@code Rectangle2D}, {@code Ellipse2D}, {@code Arc2D} and 
732     * {@code Path2D}. All other shapes are mapped to a path outline and then
733     * drawn.
734     * 
735     * @param s  the shape ({@code null} not permitted).
736     * 
737     * @see #fill(java.awt.Shape) 
738     */
739    @Override
740    public void draw(Shape s) {
741        // if the current stroke is not a BasicStroke then it is handled as
742        // a special case
743        if (!(this.stroke instanceof BasicStroke)) {
744            fill(this.stroke.createStrokedShape(s));
745            return;
746        }
747        if (s instanceof Line2D) {
748            Line2D l = (Line2D) s;
749            Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
750            if (hint != RenderingHints.VALUE_STROKE_PURE) {
751                double x1 = Math.rint(l.getX1()) - 0.5;
752                double y1 = Math.rint(l.getY1()) - 0.5;
753                double x2 = Math.rint(l.getX2()) - 0.5;
754                double y2 = Math.rint(l.getY2()) - 0.5;
755                l = line(x1, y1, x2, y2);
756            }
757            this.gc.strokeLine(l.getX1(), l.getY1(), l.getX2(), l.getY2());
758        } else if (s instanceof Rectangle2D) {
759            Rectangle2D r = (Rectangle2D) s;
760            if (s instanceof Rectangle) {
761                r = new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), 
762                        r.getHeight());
763            }
764            Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
765            if (hint != RenderingHints.VALUE_STROKE_PURE) {
766                double x = Math.rint(r.getX()) - 0.5;
767                double y = Math.rint(r.getY()) - 0.5;
768                double w = Math.floor(r.getWidth());
769                double h = Math.floor(r.getHeight());
770                r = rect(x, y, w, h);
771            }
772            this.gc.strokeRect(r.getX(), r.getY(), r.getWidth(), r.getHeight());
773        } else if (s instanceof RoundRectangle2D) {
774            RoundRectangle2D rr = (RoundRectangle2D) s;
775            this.gc.strokeRoundRect(rr.getX(), rr.getY(), rr.getWidth(), 
776                    rr.getHeight(), rr.getArcWidth(), rr.getArcHeight());
777        } else if (s instanceof Ellipse2D) {
778            Ellipse2D e = (Ellipse2D) s;
779            this.gc.strokeOval(e.getX(), e.getY(), e.getWidth(), e.getHeight());
780        } else if (s instanceof Arc2D) {
781            Arc2D a = (Arc2D) s;
782            this.gc.strokeArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), 
783                    a.getAngleStart(), a.getAngleExtent(), 
784                    intToArcType(a.getArcType()));
785        } else {
786            shapeToPath(s);
787            this.gc.stroke();
788        }
789    }
790
791    private final double[] coords = new double[6];
792    
793    /**
794     * Maps a shape to a path in the graphics context. 
795     * 
796     * @param s  the shape ({@code null} not permitted).
797     */
798    private void shapeToPath(Shape s) {
799        if (s instanceof Path2D) {
800            Path2D path = (Path2D) s;
801            if (path.getWindingRule() == Path2D.WIND_EVEN_ODD) {
802                this.gc.setFillRule(FillRule.EVEN_ODD);
803            } else {
804                this.gc.setFillRule(FillRule.NON_ZERO);
805            }
806        }
807        this.gc.beginPath();
808        PathIterator iterator = s.getPathIterator(null);
809        while (!iterator.isDone()) {
810            int segType = iterator.currentSegment(coords);
811            switch (segType) {
812                case PathIterator.SEG_MOVETO:
813                    this.gc.moveTo(coords[0], coords[1]);
814                    break;
815                case PathIterator.SEG_LINETO:
816                    this.gc.lineTo(coords[0], coords[1]);
817                    break;
818                case PathIterator.SEG_QUADTO:
819                    this.gc.quadraticCurveTo(coords[0], coords[1], coords[2], 
820                            coords[3]);
821                    break;
822                case PathIterator.SEG_CUBICTO:
823                    this.gc.bezierCurveTo(coords[0], coords[1], coords[2], 
824                            coords[3], coords[4], coords[5]);
825                    break;
826                case PathIterator.SEG_CLOSE:
827                    this.gc.closePath();
828                    break;
829                default:
830                    throw new RuntimeException("Unrecognised segment type " 
831                            + segType);
832            }
833            iterator.next();
834        }
835    }
836    
837    private ArcType intToArcType(int t) {
838        if (t == Arc2D.CHORD) {
839            return ArcType.CHORD;
840        } else if (t == Arc2D.OPEN) {
841            return ArcType.OPEN;
842        } else if (t == Arc2D.PIE) {
843            return ArcType.ROUND;
844        }
845        throw new IllegalArgumentException("Unrecognised t: " + t);
846    }
847    
848    /**
849     * Fills the specified shape with the current {@code paint}.  There is
850     * direct handling for {@code RoundRectangle2D}, 
851     * {@code Rectangle2D}, {@code Ellipse2D} and {@code Arc2D}.  
852     * All other shapes are mapped to a path outline and then filled.
853     * 
854     * @param s  the shape ({@code null} not permitted). 
855     * 
856     * @see #draw(java.awt.Shape) 
857     */
858    @Override
859    public void fill(Shape s) {
860        if (s instanceof Rectangle2D) {
861            Rectangle2D r = (Rectangle2D) s;
862            this.gc.fillRect(r.getX(), r.getY(), r.getWidth(), r.getHeight());
863        } else if (s instanceof RoundRectangle2D) {
864            RoundRectangle2D rr = (RoundRectangle2D) s;
865            this.gc.fillRoundRect(rr.getX(), rr.getY(), rr.getWidth(), 
866                    rr.getHeight(), rr.getArcWidth(), rr.getArcHeight());
867        } else if (s instanceof Ellipse2D) {
868            Ellipse2D e = (Ellipse2D) s;
869            this.gc.fillOval(e.getX(), e.getY(), e.getWidth(), e.getHeight());
870        } else if (s instanceof Arc2D) {
871            Arc2D a = (Arc2D) s;
872            this.gc.fillArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), 
873                    a.getAngleStart(), a.getAngleExtent(), 
874                    intToArcType(a.getArcType()));
875        } else {
876            shapeToPath(s);
877            this.gc.fill();
878        }
879    }
880
881    /**
882     * Returns the current font used for drawing text.
883     * 
884     * @return The current font (never {@code null}).
885     * 
886     * @see #setFont(java.awt.Font) 
887     */
888    @Override
889    public Font getFont() {
890        return this.font;
891    }
892
893    /**
894     * Sets the font to be used for drawing text.
895     * 
896     * @param font  the font ({@code null} is permitted but ignored).
897     * 
898     * @see #getFont() 
899     */
900    @Override
901    public void setFont(Font font) {
902        if (font == null || this.font.equals(font)) {
903            return;
904        }
905        applyFont(font);
906    }
907
908    private void applyFont(Font font) {
909        this.font = font;
910        FontWeight weight = font.isBold() ? FontWeight.BOLD : FontWeight.NORMAL;
911        FontPosture posture = font.isItalic() 
912                ? FontPosture.ITALIC : FontPosture.REGULAR;
913        javafx.scene.text.Font jfxfont = javafx.scene.text.Font.font(
914                font.getFamily(), weight, posture, font.getSize());
915        this.gc.setFont(jfxfont);
916    }
917
918    /**
919     * Returns the font metrics for the specified font.  The font metrics 
920     * returned are from Java2D (via an internal {@code BufferedImage}) which 
921     * does not always match exactly the font metrics used by JavaFX.
922     * 
923     * @param f  the font.
924     * 
925     * @return The font metrics. 
926     */
927    @Override
928    public FontMetrics getFontMetrics(Font f) {
929        if (getRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS) == Boolean.TRUE) {
930            if (this.fxFontMetrics == null 
931                    || !f.equals(this.fxFontMetrics.getFont())) {
932                this.fxFontMetrics = new FXFontMetrics(this.font, this);
933            }
934            return this.fxFontMetrics;
935        } 
936        
937        // be lazy about creating the underlying objects...
938        if (this.fmImage == null) {
939            this.fmImage = new BufferedImage(10, 10,
940                    BufferedImage.TYPE_INT_RGB);
941            this.fmImageG2 = this.fmImage.createGraphics();
942            this.fmImageG2.setRenderingHint(
943                    RenderingHints.KEY_FRACTIONALMETRICS,
944                    RenderingHints.VALUE_FRACTIONALMETRICS_ON);
945        }
946        return this.fmImageG2.getFontMetrics(f);
947    }
948    
949    /**
950     * Returns the font render context.  The implementation here returns the
951     * {@code FontRenderContext} for an image that is maintained 
952     * internally (as for {@link #getFontMetrics}).
953     * 
954     * @return The font render context.
955     */
956    @Override
957    public FontRenderContext getFontRenderContext() {
958        return this.fontRenderContext;
959    }
960
961    /**
962     * Draws a string at {@code (x, y)}.  The start of the text at the
963     * baseline level will be aligned with the {@code (x, y)} point.
964     * 
965     * @param str  the string ({@code null} not permitted).
966     * @param x  the x-coordinate.
967     * @param y  the y-coordinate.
968     * 
969     * @see #drawString(java.lang.String, float, float) 
970     */
971    @Override
972    public void drawString(String str, int x, int y) {
973        drawString(str, (float) x, (float) y);
974    }
975
976    /**
977     * Draws a string at {@code (x, y)}. The start of the text at the
978     * baseline level will be aligned with the {@code (x, y)} point.
979     * 
980     * @param str  the string ({@code null} not permitted).
981     * @param x  the x-coordinate.
982     * @param y  the y-coordinate.
983     */
984    @Override
985    public void drawString(String str, float x, float y) {
986        if (str == null) {
987            throw new NullPointerException("Null 'str' argument.");
988        }
989        this.gc.fillText(str, x, y);
990    }
991
992    /**
993     * Draws a string of attributed characters at {@code (x, y)}.  The 
994     * call is delegated to 
995     * {@link #drawString(AttributedCharacterIterator, float, float)}. 
996     * 
997     * @param iterator  an iterator for the characters.
998     * @param x  the x-coordinate.
999     * @param y  the x-coordinate.
1000     */
1001    @Override
1002    public void drawString(AttributedCharacterIterator iterator, int x, int y) {
1003        drawString(iterator, (float) x, (float) y); 
1004    }
1005
1006    /**
1007     * Draws a string of attributed characters at {@code (x, y)}. 
1008     * 
1009     * @param iterator  an iterator over the characters ({@code null} not 
1010     *     permitted).
1011     * @param x  the x-coordinate.
1012     * @param y  the y-coordinate.
1013     */
1014    @Override
1015    public void drawString(AttributedCharacterIterator iterator, float x, 
1016            float y) {
1017        Set<AttributedCharacterIterator.Attribute> 
1018                s = iterator.getAllAttributeKeys();
1019        if (!s.isEmpty()) {
1020            TextLayout layout = new TextLayout(iterator, 
1021                    getFontRenderContext());
1022            layout.draw(this, x, y);
1023        } else {
1024            StringBuilder strb = new StringBuilder();
1025            iterator.first();
1026            for (int i = iterator.getBeginIndex(); i < iterator.getEndIndex(); 
1027                    i++) {
1028                strb.append(iterator.current());
1029                iterator.next();
1030            }
1031            drawString(strb.toString(), x, y);
1032        }
1033    }
1034
1035    /**
1036     * Draws the specified glyph vector at the location {@code (x, y)}.
1037     * 
1038     * @param g  the glyph vector ({@code null} not permitted).
1039     * @param x  the x-coordinate.
1040     * @param y  the y-coordinate.
1041     */
1042    @Override
1043    public void drawGlyphVector(GlyphVector g, float x, float y) {
1044        fill(g.getOutline(x, y));
1045    }
1046
1047    /**
1048     * Applies the translation {@code (tx, ty)}.  This call is delegated 
1049     * to {@link #translate(double, double)}.
1050     * 
1051     * @param tx  the x-translation.
1052     * @param ty  the y-translation.
1053     * 
1054     * @see #translate(double, double) 
1055     */
1056    @Override
1057    public void translate(int tx, int ty) {
1058        translate(tx, (double) ty);
1059    }
1060
1061    /**
1062     * Applies the translation {@code (tx, ty)}.
1063     * 
1064     * @param tx  the x-translation.
1065     * @param ty  the y-translation.
1066     */
1067    @Override
1068    public void translate(double tx, double ty) {
1069        this.transform.translate(tx, ty);
1070        this.gc.translate(tx, ty);
1071    }
1072
1073    /**
1074     * Applies a rotation (anti-clockwise) about {@code (0, 0)}.
1075     * 
1076     * @param theta  the rotation angle (in radians). 
1077     */
1078    @Override
1079    public void rotate(double theta) {
1080        this.transform.rotate(theta);
1081        this.gc.rotate(Math.toDegrees(theta));
1082    }
1083
1084    /**
1085     * Applies a rotation (anti-clockwise) about {@code (x, y)}.
1086     * 
1087     * @param theta  the rotation angle (in radians).
1088     * @param x  the x-coordinate.
1089     * @param y  the y-coordinate.
1090     */
1091    @Override
1092    public void rotate(double theta, double x, double y) {
1093        translate(x, y);
1094        rotate(theta);
1095        translate(-x, -y);
1096    }
1097
1098    /**
1099     * Applies a scale transformation.
1100     * 
1101     * @param sx  the x-scaling factor.
1102     * @param sy  the y-scaling factor.
1103     */
1104    @Override
1105    public void scale(double sx, double sy) {
1106        this.transform.scale(sx, sy);
1107        this.gc.scale(sx, sy);
1108    }
1109
1110    /**
1111     * Applies a shear transformation. This is equivalent to the following 
1112     * call to the {@code transform} method:
1113     * <br><br>
1114     * <ul><li>
1115     * {@code transform(AffineTransform.getShearInstance(shx, shy));}
1116     * </ul>
1117     * 
1118     * @param shx  the x-shear factor.
1119     * @param shy  the y-shear factor.
1120     */
1121    @Override
1122    public void shear(double shx, double shy) {
1123        transform(AffineTransform.getShearInstance(shx, shy));
1124    }
1125
1126    /**
1127     * Applies this transform to the existing transform by concatenating it.
1128     * 
1129     * @param t  the transform ({@code null} not permitted). 
1130     */
1131    @Override
1132    public void transform(AffineTransform t) {
1133        AffineTransform tx = getTransform();
1134        tx.concatenate(t);
1135        setTransform(tx);
1136    }
1137
1138    /**
1139     * Returns a copy of the current transform.
1140     * 
1141     * @return A copy of the current transform (never {@code null}).
1142     * 
1143     * @see #setTransform(java.awt.geom.AffineTransform) 
1144     */
1145    @Override
1146    public AffineTransform getTransform() {
1147        return (AffineTransform) this.transform.clone();
1148    }
1149
1150    /**
1151     * Sets the transform.
1152     * 
1153     * @param t  the new transform ({@code null} permitted, resets to the
1154     *     identity transform).
1155     * 
1156     * @see #getTransform() 
1157     */
1158    @Override
1159    public void setTransform(AffineTransform t) {
1160        if (t == null) {
1161            this.transform = new AffineTransform();
1162            t = this.transform;
1163        } else {
1164            this.transform = new AffineTransform(t);
1165        }
1166        this.gc.setTransform(t.getScaleX(), t.getShearY(), t.getShearX(), 
1167                t.getScaleY(), t.getTranslateX(), t.getTranslateY());
1168    }
1169
1170    /**
1171     * Returns {@code true} if the rectangle (in device space) intersects
1172     * with the shape (the interior, if {@code onStroke} is {@code false}, 
1173     * otherwise the stroked outline of the shape).
1174     * 
1175     * @param rect  a rectangle (in device space).
1176     * @param s the shape.
1177     * @param onStroke  test the stroked outline only?
1178     * 
1179     * @return A boolean. 
1180     */
1181    @Override
1182    public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
1183        Shape ts;
1184        if (onStroke) {
1185            ts = this.transform.createTransformedShape(
1186                    this.stroke.createStrokedShape(s));
1187        } else {
1188            ts = this.transform.createTransformedShape(s);
1189        }
1190        if (!rect.getBounds2D().intersects(ts.getBounds2D())) {
1191            return false;
1192        }
1193        Area a1 = new Area(rect);
1194        Area a2 = new Area(ts);
1195        a1.intersect(a2);
1196        return !a1.isEmpty();
1197    }
1198
1199    /**
1200     * Not implemented - the method does nothing.
1201     */
1202    @Override
1203    public void setPaintMode() {
1204        // not implemented
1205    }
1206
1207    /**
1208     * Not implemented - the method does nothing.
1209     */
1210    @Override
1211    public void setXORMode(Color c1) {
1212        // not implemented
1213    }
1214
1215    /**
1216     * Returns the bounds of the user clipping region.
1217     * 
1218     * @return The clip bounds (possibly {@code null}). 
1219     * 
1220     * @see #getClip() 
1221     */
1222    @Override
1223    public Rectangle getClipBounds() {
1224        if (this.clip == null) {
1225            return null;
1226        }
1227        return getClip().getBounds();
1228    }
1229
1230    /**
1231     * Returns the user clipping region.  The initial default value is 
1232     * {@code null}.
1233     * 
1234     * @return The user clipping region (possibly {@code null}).
1235     * 
1236     * @see #setClip(java.awt.Shape)
1237     */
1238    @Override
1239    public Shape getClip() {
1240        if (this.clip == null) {
1241            return null;
1242        }
1243        AffineTransform inv;
1244        try {
1245            inv = this.transform.createInverse();
1246            return inv.createTransformedShape(this.clip);
1247        } catch (NoninvertibleTransformException ex) {
1248            return null;
1249        }
1250    }
1251
1252    /**
1253     * Sets the user clipping region.
1254     * 
1255     * @param shape  the new user clipping region ({@code null} permitted).
1256     * 
1257     * @see #getClip()
1258     */
1259    @Override
1260    public void setClip(Shape shape) {
1261        if (this.stateSaved) {
1262            this.gc.restore(); // get back original clip
1263            reapplyAttributes(); // but keep other attributes
1264            this.stateSaved = false;
1265        }
1266        // null is handled fine here...
1267        this.clip = this.transform.createTransformedShape(shape);
1268        if (clip != null) {
1269            this.gc.save(); 
1270            rememberSavedAttributes();
1271            shapeToPath(shape);
1272            this.gc.clip();
1273        }
1274    }
1275    
1276    /** 
1277     * Remember the Graphics2D attributes in force at the point of pushing
1278     * the JavaFX context.
1279     */
1280    private void rememberSavedAttributes() {
1281        this.stateSaved = true;
1282        this.savedColor = this.color;
1283        this.savedFont = this.font;
1284        this.savedPaint = this.paint;
1285        this.savedStroke = this.stroke;
1286        this.savedTransform = new AffineTransform(this.transform);
1287    }
1288    
1289    private void reapplyAttributes() {
1290        if (!paintsAreEqual(this.paint, this.savedPaint)) {
1291            applyPaint(this.paint);
1292        }
1293        if (!this.color.equals(this.savedColor)) {
1294            applyColor(this.color);
1295        }
1296        if (!this.stroke.equals(this.savedStroke)) {
1297            applyStroke(this.stroke);
1298        }
1299        if (!this.font.equals(this.savedFont)) {
1300            applyFont(this.font);
1301        }
1302        if (!this.transform.equals(this.savedTransform)) {
1303            setTransform(this.transform);
1304        }
1305        this.savedColor = null;
1306        this.savedFont = null;
1307        this.savedPaint = null;
1308        this.savedStroke = null;
1309        this.savedTransform = null;
1310    }
1311    
1312    /**
1313     * Clips to the intersection of the current clipping region and the
1314     * specified shape. 
1315     * 
1316     * According to the Oracle API specification, this method will accept a 
1317     * {@code null} argument, but there is an open bug report (since 2004) 
1318     * that suggests this is wrong:
1319     * <p>
1320     * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189">
1321     * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189</a>
1322     * 
1323     * In this implementation, a {@code null} argument is not permitted.
1324     * 
1325     * @param s  the clip shape ({@code null} not permitted). 
1326     */
1327    @Override
1328    public void clip(Shape s) {
1329        if (s instanceof Line2D) {
1330            s = s.getBounds2D();
1331        }
1332        if (this.clip == null) {
1333            setClip(s);
1334            return;
1335        }
1336        Shape ts = this.transform.createTransformedShape(s);
1337        Shape clipNew;
1338        if (!ts.intersects(this.clip.getBounds2D())) {
1339            clipNew = new Rectangle2D.Double();
1340        } else {
1341            Area a1 = new Area(ts);
1342            Area a2 = new Area(this.clip);
1343            a1.intersect(a2);
1344            clipNew = new Path2D.Double(a1);
1345        }
1346        this.clip = clipNew;
1347        if (!this.stateSaved) {
1348            this.gc.save();
1349            rememberSavedAttributes();
1350        }
1351        shapeToPath(this.clip);
1352        this.gc.clip();
1353    }
1354
1355    /**
1356     * Clips to the intersection of the current clipping region and the 
1357     * specified rectangle.
1358     * 
1359     * @param x  the x-coordinate.
1360     * @param y  the y-coordinate.
1361     * @param width  the width.
1362     * @param height  the height.
1363     */
1364    @Override
1365    public void clipRect(int x, int y, int width, int height) {
1366        clip(rect(x, y, width, height));
1367    }
1368
1369    /**
1370     * Sets the user clipping region to the specified rectangle.
1371     * 
1372     * @param x  the x-coordinate.
1373     * @param y  the y-coordinate.
1374     * @param width  the width.
1375     * @param height  the height.
1376     * 
1377     * @see #getClip() 
1378     */
1379    @Override
1380    public void setClip(int x, int y, int width, int height) {
1381        setClip(rect(x, y, width, height));
1382    }
1383
1384    /**
1385     * Draws a line from {@code (x1, y1)} to {@code (x2, y2)} using 
1386     * the current {@code paint} and {@code stroke}.
1387     * 
1388     * @param x1  the x-coordinate of the start point.
1389     * @param y1  the y-coordinate of the start point.
1390     * @param x2  the x-coordinate of the end point.
1391     * @param y2  the x-coordinate of the end point.
1392     */
1393    @Override
1394    public void drawLine(int x1, int y1, int x2, int y2) {
1395        draw(line(x1, y1, x2, y2));
1396    }
1397
1398    /**
1399     * Fills the specified rectangle with the current {@code paint}.
1400     * 
1401     * @param x  the x-coordinate.
1402     * @param y  the y-coordinate.
1403     * @param width  the rectangle width.
1404     * @param height  the rectangle height.
1405     */
1406    @Override
1407    public void fillRect(int x, int y, int width, int height) {
1408        fill(rect(x, y, width, height));
1409    }
1410
1411    /**
1412     * Clears the specified rectangle by filling it with the current 
1413     * background color.  If the background color is {@code null}, this
1414     * method will do nothing.
1415     * 
1416     * @param x  the x-coordinate.
1417     * @param y  the y-coordinate.
1418     * @param width  the width.
1419     * @param height  the height.
1420     * 
1421     * @see #getBackground() 
1422     */
1423    @Override
1424    public void clearRect(int x, int y, int width, int height) {
1425        if (getBackground() == null) {
1426            return;  // we can't do anything
1427        }
1428        Paint saved = getPaint();
1429        setPaint(getBackground());
1430        fillRect(x, y, width, height);
1431        setPaint(saved);
1432    }
1433    
1434    /**
1435     * Draws a rectangle with rounded corners using the current 
1436     * {@code paint} and {@code stroke}.
1437     * 
1438     * @param x  the x-coordinate.
1439     * @param y  the y-coordinate.
1440     * @param width  the width.
1441     * @param height  the height.
1442     * @param arcWidth  the arc-width.
1443     * @param arcHeight  the arc-height.
1444     * 
1445     * @see #fillRoundRect(int, int, int, int, int, int) 
1446     */
1447    @Override
1448    public void drawRoundRect(int x, int y, int width, int height, 
1449            int arcWidth, int arcHeight) {
1450        draw(roundRect(x, y, width, height, arcWidth, arcHeight));
1451    }
1452
1453    /**
1454     * Fills a rectangle with rounded corners using the current {@code paint}.
1455     * 
1456     * @param x  the x-coordinate.
1457     * @param y  the y-coordinate.
1458     * @param width  the width.
1459     * @param height  the height.
1460     * @param arcWidth  the arc-width.
1461     * @param arcHeight  the arc-height.
1462     * 
1463     * @see #drawRoundRect(int, int, int, int, int, int) 
1464     */
1465    @Override
1466    public void fillRoundRect(int x, int y, int width, int height, 
1467            int arcWidth, int arcHeight) {
1468        fill(roundRect(x, y, width, height, arcWidth, arcHeight));
1469    }
1470    
1471    /**
1472     * Draws an oval framed by the rectangle {@code (x, y, width, height)}
1473     * using the current {@code paint} and {@code stroke}.
1474     * 
1475     * @param x  the x-coordinate.
1476     * @param y  the y-coordinate.
1477     * @param width  the width.
1478     * @param height  the height.
1479     * 
1480     * @see #fillOval(int, int, int, int) 
1481     */
1482    @Override
1483    public void drawOval(int x, int y, int width, int height) {
1484        draw(oval(x, y, width, height));
1485    }
1486
1487    /**
1488     * Fills an oval framed by the rectangle {@code (x, y, width, height)}.
1489     * 
1490     * @param x  the x-coordinate.
1491     * @param y  the y-coordinate.
1492     * @param width  the width.
1493     * @param height  the height.
1494     * 
1495     * @see #drawOval(int, int, int, int) 
1496     */
1497    @Override
1498    public void fillOval(int x, int y, int width, int height) {
1499        fill(oval(x, y, width, height));
1500    }
1501
1502    /**
1503     * Draws an arc contained within the rectangle 
1504     * {@code (x, y, width, height)}, starting at {@code startAngle}
1505     * and continuing through {@code arcAngle} degrees using 
1506     * the current {@code paint} and {@code stroke}.
1507     * 
1508     * @param x  the x-coordinate.
1509     * @param y  the y-coordinate.
1510     * @param width  the width.
1511     * @param height  the height.
1512     * @param startAngle  the start angle in degrees, 0 = 3 o'clock.
1513     * @param arcAngle  the angle (anticlockwise) in degrees.
1514     * 
1515     * @see #fillArc(int, int, int, int, int, int) 
1516     */
1517    @Override
1518    public void drawArc(int x, int y, int width, int height, int startAngle, 
1519            int arcAngle) {
1520        draw(arc(x, y, width, height, startAngle, arcAngle));
1521    }
1522
1523    /**
1524     * Fills an arc contained within the rectangle 
1525     * {@code (x, y, width, height)}, starting at {@code startAngle}
1526     * and continuing through {@code arcAngle} degrees, using 
1527     * the current {@code paint}.
1528     * 
1529     * @param x  the x-coordinate.
1530     * @param y  the y-coordinate.
1531     * @param width  the width.
1532     * @param height  the height.
1533     * @param startAngle  the start angle in degrees, 0 = 3 o'clock.
1534     * @param arcAngle  the angle (anticlockwise) in degrees.
1535     * 
1536     * @see #drawArc(int, int, int, int, int, int) 
1537     */
1538    @Override
1539    public void fillArc(int x, int y, int width, int height, int startAngle, 
1540            int arcAngle) {
1541        fill(arc(x, y, width, height, startAngle, arcAngle));
1542    }
1543
1544    /**
1545     * Draws the specified multi-segment line using the current 
1546     * {@code paint} and {@code stroke}.
1547     * 
1548     * @param xPoints  the x-points.
1549     * @param yPoints  the y-points.
1550     * @param nPoints  the number of points to use for the polyline.
1551     */
1552    @Override
1553    public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
1554        GeneralPath p = createPolygon(xPoints, yPoints, nPoints, false);
1555        draw(p);
1556    }
1557
1558    /**
1559     * Draws the specified polygon using the current {@code paint} and 
1560     * {@code stroke}.
1561     * 
1562     * @param xPoints  the x-points.
1563     * @param yPoints  the y-points.
1564     * @param nPoints  the number of points to use for the polygon.
1565     * 
1566     * @see #fillPolygon(int[], int[], int)      */
1567    @Override
1568    public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {
1569        GeneralPath p = createPolygon(xPoints, yPoints, nPoints, true);
1570        draw(p);
1571    }
1572
1573    /**
1574     * Fills the specified polygon using the current {@code paint}.
1575     * 
1576     * @param xPoints  the x-points.
1577     * @param yPoints  the y-points.
1578     * @param nPoints  the number of points to use for the polygon.
1579     * 
1580     * @see #drawPolygon(int[], int[], int) 
1581     */
1582    @Override
1583    public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
1584        GeneralPath p = createPolygon(xPoints, yPoints, nPoints, true);
1585        fill(p);
1586    }
1587
1588    /**
1589     * Creates a polygon from the specified {@code x} and 
1590     * {@code y} coordinate arrays.
1591     * 
1592     * @param xPoints  the x-points.
1593     * @param yPoints  the y-points.
1594     * @param nPoints  the number of points to use for the polyline.
1595     * @param close  closed?
1596     * 
1597     * @return A polygon.
1598     */
1599    public GeneralPath createPolygon(int[] xPoints, int[] yPoints, 
1600            int nPoints, boolean close) {
1601        GeneralPath p = new GeneralPath();
1602        p.moveTo(xPoints[0], yPoints[0]);
1603        for (int i = 1; i < nPoints; i++) {
1604            p.lineTo(xPoints[i], yPoints[i]);
1605        }
1606        if (close) {
1607            p.closePath();
1608        }
1609        return p;
1610    }
1611    
1612    /**
1613     * Draws an image at the location {@code (x, y)}.  Note that the 
1614     * {@code observer} is ignored.
1615     * 
1616     * @param img  the image ({@code null} permitted...method will do nothing).
1617     * @param x  the x-coordinate.
1618     * @param y  the y-coordinate.
1619     * @param observer  ignored.
1620     * 
1621     * @return {@code true} if there is no more drawing to be done. 
1622     */
1623    @Override
1624    public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
1625        if (img == null) {
1626            return true;
1627        }
1628        int w = img.getWidth(observer);
1629        if (w < 0) {
1630            return false;
1631        }
1632        int h = img.getHeight(observer);
1633        if (h < 0) {
1634            return false;
1635        }
1636        return drawImage(img, x, y, w, h, observer);
1637    }
1638
1639    /**
1640     * Draws the image into the rectangle defined by {@code (x, y, w, h)}.  
1641     * Note that the {@code observer} is ignored (it is not useful in this
1642     * context).
1643     * 
1644     * @param img  the image ({@code null} permitted...draws nothing).
1645     * @param x  the x-coordinate.
1646     * @param y  the y-coordinate.
1647     * @param w  the width.
1648     * @param h  the height.
1649     * @param observer  ignored.
1650     * 
1651     * @return {@code true} if there is no more drawing to be done. 
1652     */
1653    @Override
1654    public boolean drawImage(final Image img, int x, int y, 
1655            int w, int h, ImageObserver observer) {
1656        final BufferedImage buffered;
1657        if (img instanceof BufferedImage) {
1658            buffered = (BufferedImage) img;
1659        } else {
1660            buffered = new BufferedImage(w, h, 
1661                    BufferedImage.TYPE_INT_ARGB);
1662            final Graphics2D g2 = buffered.createGraphics();
1663            g2.drawImage(img, 0, 0, w, h, null);
1664            g2.dispose();
1665        }
1666        javafx.scene.image.WritableImage fxImage = SwingFXUtils.toFXImage(
1667                buffered, null);
1668        this.gc.drawImage(fxImage, x, y, w, h);
1669        return true;
1670    }
1671
1672    /**
1673     * Draws an image at the location {@code (x, y)}.  Note that the 
1674     * {@code observer} is ignored.
1675     * 
1676     * @param img  the image ({@code null} permitted...draws nothing).
1677     * @param x  the x-coordinate.
1678     * @param y  the y-coordinate.
1679     * @param bgcolor  the background color ({@code null} permitted).
1680     * @param observer  ignored.
1681     * 
1682     * @return {@code true} if there is no more drawing to be done. 
1683     */
1684    @Override
1685    public boolean drawImage(Image img, int x, int y, Color bgcolor, 
1686            ImageObserver observer) {
1687        if (img == null) {
1688            return true;
1689        }
1690        int w = img.getWidth(null);
1691        if (w < 0) {
1692            return false;
1693        }
1694        int h = img.getHeight(null);
1695        if (h < 0) {
1696            return false;
1697        }
1698        return drawImage(img, x, y, w, h, bgcolor, observer);
1699    }
1700
1701    /**
1702     * Draws an image to the rectangle {@code (x, y, w, h)} (scaling it if
1703     * required), first filling the background with the specified color.  Note 
1704     * that the {@code observer} is ignored.
1705     * 
1706     * @param img  the image.
1707     * @param x  the x-coordinate.
1708     * @param y  the y-coordinate.
1709     * @param w  the width.
1710     * @param h  the height.
1711     * @param bgcolor  the background color ({@code null} permitted).
1712     * @param observer  ignored.
1713     * 
1714     * @return {@code true} if the image is drawn.      
1715     */
1716    @Override
1717    public boolean drawImage(Image img, int x, int y, int w, int h, 
1718            Color bgcolor, ImageObserver observer) {
1719        Paint saved = getPaint();
1720        setPaint(bgcolor);
1721        fillRect(x, y, w, h);
1722        setPaint(saved);
1723        return drawImage(img, x, y, w, h, observer);
1724    }
1725
1726    /**
1727     * Draws part of an image (defined by the source rectangle 
1728     * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle
1729     * {@code (dx1, dy1, dx2, dy2)}.  Note that the {@code observer} 
1730     * is ignored in this implementation.
1731     * 
1732     * @param img  the image.
1733     * @param dx1  the x-coordinate for the top left of the destination.
1734     * @param dy1  the y-coordinate for the top left of the destination.
1735     * @param dx2  the x-coordinate for the bottom right of the destination.
1736     * @param dy2  the y-coordinate for the bottom right of the destination.
1737     * @param sx1  the x-coordinate for the top left of the source.
1738     * @param sy1  the y-coordinate for the top left of the source.
1739     * @param sx2  the x-coordinate for the bottom right of the source.
1740     * @param sy2  the y-coordinate for the bottom right of the source.
1741     * 
1742     * @return {@code true} if the image is drawn. 
1743     */
1744    @Override
1745    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 
1746            int sx1, int sy1, int sx2, int sy2, ImageObserver observer) {
1747        int w = dx2 - dx1;
1748        int h = dy2 - dy1;
1749        BufferedImage img2 = new BufferedImage(w, h, 
1750                BufferedImage.TYPE_INT_ARGB);
1751        Graphics2D g2 = img2.createGraphics();
1752        g2.drawImage(img, 0, 0, w, h, sx1, sy1, sx2, sy2, null);
1753        return drawImage(img2, dx1, dy1, null);
1754    }
1755
1756    /**
1757     * Draws part of an image (defined by the source rectangle 
1758     * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle
1759     * {@code (dx1, dy1, dx2, dy2)}.  The destination rectangle is first
1760     * cleared by filling it with the specified {@code bgcolor}. Note that
1761     * the {@code observer} is ignored. 
1762     * 
1763     * @param img  the image.
1764     * @param dx1  the x-coordinate for the top left of the destination.
1765     * @param dy1  the y-coordinate for the top left of the destination.
1766     * @param dx2  the x-coordinate for the bottom right of the destination.
1767     * @param dy2  the y-coordinate for the bottom right of the destination.
1768     * @param sx1 the x-coordinate for the top left of the source.
1769     * @param sy1 the y-coordinate for the top left of the source.
1770     * @param sx2 the x-coordinate for the bottom right of the source.
1771     * @param sy2 the y-coordinate for the bottom right of the source.
1772     * @param bgcolor  the background color ({@code null} permitted).
1773     * @param observer  ignored.
1774     * 
1775     * @return {@code true} if the image is drawn. 
1776     */
1777    @Override
1778    public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 
1779            int sx1, int sy1, int sx2, int sy2, Color bgcolor, 
1780            ImageObserver observer) {
1781        Paint saved = getPaint();
1782        setPaint(bgcolor);
1783        fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1);
1784        setPaint(saved);
1785        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer);
1786    }
1787
1788    /**
1789     * Draws the rendered image. When {@code img} is {@code null} this method
1790     * does nothing.
1791     * 
1792     * @param img  the image ({@code null} permitted).
1793     * @param xform  the transform.
1794     */
1795    @Override
1796    public void drawRenderedImage(RenderedImage img, AffineTransform xform) {
1797        if (img == null) { // to match the behaviour specified in the JDK
1798            return;
1799        }
1800        BufferedImage bi = convertRenderedImage(img);
1801        drawImage(bi, xform, null);
1802    }
1803
1804    /**
1805     * Converts a rendered image to a {@code BufferedImage}.  This utility
1806     * method has come from a forum post by Jim Moore at:
1807     * <p>
1808     * <a href="http://www.jguru.com/faq/view.jsp?EID=114602">
1809     * http://www.jguru.com/faq/view.jsp?EID=114602</a>
1810     * 
1811     * @param img  the rendered image.
1812     * 
1813     * @return A buffered image. 
1814     */
1815    private static BufferedImage convertRenderedImage(RenderedImage img) {
1816        if (img instanceof BufferedImage) {
1817            return (BufferedImage) img; 
1818        }
1819        ColorModel cm = img.getColorModel();
1820        int width = img.getWidth();
1821        int height = img.getHeight();
1822        WritableRaster raster = cm.createCompatibleWritableRaster(width, height);
1823        boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
1824        Hashtable properties = new Hashtable();
1825        String[] keys = img.getPropertyNames();
1826        if (keys != null) {
1827            for (String key : keys) {
1828                properties.put(key, img.getProperty(key));
1829            }
1830        }
1831        BufferedImage result = new BufferedImage(cm, raster, 
1832                isAlphaPremultiplied, properties);
1833        img.copyData(raster);
1834        return result;
1835    }
1836
1837    /**
1838     * Draws the renderable image.
1839     * 
1840     * @param img  the renderable image.
1841     * @param xform  the transform.
1842     */
1843    @Override
1844    public void drawRenderableImage(RenderableImage img, 
1845            AffineTransform xform) {
1846        RenderedImage ri = img.createDefaultRendering();
1847        drawRenderedImage(ri, xform);
1848    }
1849
1850    /**
1851     * Draws an image with the specified transform. Note that the 
1852     * {@code observer} is ignored in this implementation.     
1853     * 
1854     * @param img  the image.
1855     * @param xform  the transform ({@code null} permitted).
1856     * @param obs  the image observer (ignored).
1857     * 
1858     * @return {@code true} if the image is drawn. 
1859     */
1860    @Override
1861    public boolean drawImage(Image img, AffineTransform xform, 
1862            ImageObserver obs) {
1863        AffineTransform savedTransform = getTransform();
1864        if (xform != null) {
1865            transform(xform);
1866        }
1867        boolean result = drawImage(img, 0, 0, obs);
1868        if (xform != null) {
1869            setTransform(savedTransform);
1870        }
1871        return result;
1872    }
1873
1874    /**
1875     * Draws the image resulting from applying the {@code BufferedImageOp}
1876     * to the specified image at the location {@code (x, y)}.
1877     * 
1878     * @param img  the image.
1879     * @param op  the operation ({@code null} permitted).
1880     * @param x  the x-coordinate.
1881     * @param y  the y-coordinate.
1882     */
1883    @Override
1884    public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
1885        BufferedImage imageToDraw = img;
1886        if (op != null) {
1887            imageToDraw = op.filter(img, null);
1888        }
1889        drawImage(imageToDraw, new AffineTransform(1f, 0f, 0f, 1f, x, y), null);
1890    }
1891
1892    /**
1893     * Not yet implemented.
1894     * 
1895     * @param x  the x-coordinate.
1896     * @param y  the y-coordinate.
1897     * @param width  the width of the area.
1898     * @param height  the height of the area.
1899     * @param dx  the delta x.
1900     * @param dy  the delta y.
1901     */
1902    @Override
1903    public void copyArea(int x, int y, int width, int height, int dx, int dy) {
1904        // FIXME: implement this, low priority
1905    }
1906
1907    /**
1908     * This method does nothing.
1909     */
1910    @Override
1911    public void dispose() {
1912        // nothing to do
1913    }
1914 
1915    /**
1916     * Returns a recyclable {@link Line2D} object.
1917     * 
1918     * @param x1  the x-coordinate.
1919     * @param y1  the y-coordinate.
1920     * @param x2  the width.
1921     * @param y2  the height.
1922     * 
1923     * @return A line (never {@code null}).
1924     */
1925    private Line2D line(double x1, double y1, double x2, double y2) {
1926        if (this.line == null) {
1927            this.line = new Line2D.Double(x1, y1, x2, y2);
1928        } else {
1929            this.line.setLine(x1, y1, x2, y2);
1930        }
1931        return this.line;
1932    }
1933    
1934    /**
1935     * Sets the attributes of the reusable {@link Rectangle2D} object that is
1936     * used by the {@link FXGraphics2D#drawRect(int, int, int, int)} and 
1937     * {@link FXGraphics2D#fillRect(int, int, int, int)} methods.
1938     * 
1939     * @param x  the x-coordinate.
1940     * @param y  the y-coordinate.
1941     * @param width  the width.
1942     * @param height  the height.
1943     * 
1944     * @return A rectangle (never {@code null}).
1945     */
1946    private Rectangle2D rect(double x, double y, double width, double height) {
1947        if (this.rect == null) {
1948            this.rect = new Rectangle2D.Double(x, y, width, height);
1949        } else {
1950            this.rect.setRect(x, y, width, height);
1951        }
1952        return this.rect;
1953    }
1954
1955    /**
1956     * Sets the attributes of the reusable {@link RoundRectangle2D} object that
1957     * is used by the {@link #drawRoundRect(int, int, int, int, int, int)} and
1958     * {@link #fillRoundRect(int, int, int, int, int, int)} methods.
1959     * 
1960     * @param x  the x-coordinate.
1961     * @param y  the y-coordinate.
1962     * @param width  the width.
1963     * @param height  the height.
1964     * @param arcWidth  the arc width.
1965     * @param arcHeight  the arc height.
1966     * 
1967     * @return A round rectangle (never {@code null}).
1968     */
1969    private RoundRectangle2D roundRect(int x, int y, int width, int height, 
1970            int arcWidth, int arcHeight) {
1971        if (this.roundRect == null) {
1972            this.roundRect = new RoundRectangle2D.Double(x, y, width, height, 
1973                    arcWidth, arcHeight);
1974        } else {
1975            this.roundRect.setRoundRect(x, y, width, height, 
1976                    arcWidth, arcHeight);
1977        }
1978        return this.roundRect;
1979    }
1980
1981    /**
1982     * Sets the attributes of the reusable {@link Arc2D} object that is used by
1983     * {@link #drawArc(int, int, int, int, int, int)} and 
1984     * {@link #fillArc(int, int, int, int, int, int)} methods.
1985     * 
1986     * @param x  the x-coordinate.
1987     * @param y  the y-coordinate.
1988     * @param width  the width.
1989     * @param height  the height.
1990     * @param startAngle  the start angle in degrees, 0 = 3 o'clock.
1991     * @param arcAngle  the angle (anticlockwise) in degrees.
1992     * 
1993     * @return An arc (never {@code null}).
1994     */
1995    private Arc2D arc(int x, int y, int width, int height, int startAngle, 
1996            int arcAngle) {
1997        if (this.arc == null) {
1998            this.arc = new Arc2D.Double(x, y, width, height, startAngle, 
1999                    arcAngle, Arc2D.OPEN);
2000        } else {
2001            this.arc.setArc(x, y, width, height, startAngle, arcAngle, 
2002                    Arc2D.OPEN);
2003        }
2004        return this.arc;
2005    }
2006            
2007    /**
2008     * Returns an {@link Ellipse2D} object that may be reused (so this instance
2009     * should be used for short term operations only). See the 
2010     * {@link #drawOval(int, int, int, int)} and 
2011     * {@link #fillOval(int, int, int, int)} methods for usage.
2012     * 
2013     * @param x  the x-coordinate.
2014     * @param y  the y-coordinate.
2015     * @param width  the width.
2016     * @param height  the height.
2017     * 
2018     * @return An oval shape (never {@code null}).
2019     */
2020    private Ellipse2D oval(int x, int y, int width, int height) {
2021        if (this.oval == null) {
2022            this.oval = new Ellipse2D.Double(x, y, width, height);
2023        } else {
2024            this.oval.setFrame(x, y, width, height);
2025        }
2026        return this.oval;
2027    }
2028    
2029    /**
2030     * Returns {@code true} if the two {@code Paint} objects are equal 
2031     * OR both {@code null}.  This method handles
2032     * {@code GradientPaint}, {@code LinearGradientPaint} 
2033     * and {@code RadialGradientPaint} as special cases, since those classes do
2034     * not override the {@code equals()} method.
2035     *
2036     * @param p1  paint 1 ({@code null} permitted).
2037     * @param p2  paint 2 ({@code null} permitted).
2038     *
2039     * @return A boolean.
2040     */
2041    private static boolean paintsAreEqual(Paint p1, Paint p2) {
2042        if (p1 == p2) {
2043            return true;
2044        }
2045            
2046        // handle cases where either or both arguments are null
2047        if (p1 == null) {
2048            return (p2 == null);   
2049        }
2050        if (p2 == null) {
2051            return false;   
2052        }
2053
2054        // handle cases...
2055        if (p1 instanceof Color && p2 instanceof Color) {
2056            return p1.equals(p2);
2057        }
2058        if (p1 instanceof GradientPaint && p2 instanceof GradientPaint) {
2059            GradientPaint gp1 = (GradientPaint) p1;
2060            GradientPaint gp2 = (GradientPaint) p2;
2061            return gp1.getColor1().equals(gp2.getColor1()) 
2062                    && gp1.getColor2().equals(gp2.getColor2())
2063                    && gp1.getPoint1().equals(gp2.getPoint1())    
2064                    && gp1.getPoint2().equals(gp2.getPoint2())
2065                    && gp1.isCyclic() == gp2.isCyclic()
2066                    && gp1.getTransparency() == gp1.getTransparency(); 
2067        } 
2068        if (p1 instanceof LinearGradientPaint 
2069                && p2 instanceof LinearGradientPaint) {
2070            LinearGradientPaint lgp1 = (LinearGradientPaint) p1;
2071            LinearGradientPaint lgp2 = (LinearGradientPaint) p2;
2072            return lgp1.getStartPoint().equals(lgp2.getStartPoint())
2073                    && lgp1.getEndPoint().equals(lgp2.getEndPoint()) 
2074                    && Arrays.equals(lgp1.getFractions(), lgp2.getFractions())
2075                    && Arrays.equals(lgp1.getColors(), lgp2.getColors())
2076                    && lgp1.getCycleMethod() == lgp2.getCycleMethod()
2077                    && lgp1.getColorSpace() == lgp2.getColorSpace()
2078                    && lgp1.getTransform().equals(lgp2.getTransform());
2079        } 
2080        if (p1 instanceof RadialGradientPaint 
2081                && p2 instanceof RadialGradientPaint) {
2082            RadialGradientPaint rgp1 = (RadialGradientPaint) p1;
2083            RadialGradientPaint rgp2 = (RadialGradientPaint) p2;
2084            return rgp1.getCenterPoint().equals(rgp2.getCenterPoint())
2085                    && rgp1.getRadius() == rgp2.getRadius() 
2086                    && rgp1.getFocusPoint().equals(rgp2.getFocusPoint())
2087                    && Arrays.equals(rgp1.getFractions(), rgp2.getFractions())
2088                    && Arrays.equals(rgp1.getColors(), rgp2.getColors())
2089                    && rgp1.getCycleMethod() == rgp2.getCycleMethod()
2090                    && rgp1.getColorSpace() == rgp2.getColorSpace()
2091                    && rgp1.getTransform().equals(rgp2.getTransform());
2092        }
2093        return p1.equals(p2);
2094    }
2095
2096}