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