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