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