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