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