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