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