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