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