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