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