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}