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