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