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