All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jfree.fx.FXGraphics2D Maven / Gradle / Ivy

/* ============
 * FXGraphics2D
 * ============
 * 
 * (C)opyright 2014-2022, by David Gilbert.
 * 
 * https://github.com/jfree/fxgraphics2d
 *
 * The FXGraphics2D class has been developed by David Gilbert for
 * use in Orson Charts (https://github.com/jfree/orson-charts) and
 * JFreeChart (http://www.jfree.org/jfreechart).  It may be useful for other
 * code that uses the Graphics2D API provided by Java2D.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of JFree.org nor the names of its contributors may
 *     be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */

package org.jfree.fx;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.RenderableImage;
import java.text.AttributedCharacterIterator;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import javafx.embed.swing.SwingFXUtils;

import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BlendMode;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;

/**
 * A {@link Graphics2D} implementation that writes to a JavaFX {@link Canvas}.
 * This is intended for general purpose usage, but has been created for use in
 * Orson Charts (https://github.com/jfree/orson-charts/) and
 * JFreeChart (http://www.jfree.org/jfreechart/).
 */
public class FXGraphics2D extends Graphics2D {
    
    /** The graphics context for the JavaFX canvas. */
    private final GraphicsContext gc;
    
    /** Rendering hints. */
    private final RenderingHints hints;
    
    private Shape clip;
    
    /** Stores the AWT Paint object for get/setPaint(). */
    private Paint paint = Color.BLACK;
    
    /** Stores the AWT Color object for get/setColor(). */
    private Color color = Color.BLACK;
    
    private Composite composite = AlphaComposite.getInstance(
            AlphaComposite.SRC_OVER, 1.0f);

    private Stroke stroke = new BasicStroke(1.0f);
 
    /** 
     * The width of the stroke to use when the user supplies a
     * BasicStroke with a width of 0.0 (in this case the Java specification
     * says "If width is set to 0.0f, the stroke is rendered as the thinnest 
     * possible line for the target device and the antialias hint setting.")
     */
    private double zeroStrokeWidth;
    
    private Font font = new Font("SansSerif", Font.PLAIN, 12);
    
    private final FontRenderContext fontRenderContext = new FontRenderContext(
            null, false, true);

    private AffineTransform transform = new AffineTransform();

    /** The background color, used in the {@code clearRect()} method. */
    private Color background = Color.BLACK;
    
    /** A flag that is set when the JavaFX graphics state has been saved. */
    private boolean stateSaved = false;

    private Stroke savedStroke;
    private Paint savedPaint;
    private Color savedColor;
    private Font savedFont;
    private AffineTransform savedTransform;
    
    /**
     * An instance that is lazily instantiated in drawLine and then 
     * subsequently reused to avoid creating a lot of garbage.
     */
    private Line2D line;
    
    /**
     * An instance that is lazily instantiated in fillRect and then 
     * subsequently reused to avoid creating a lot of garbage.
     */
    Rectangle2D rect;

    /**
     * An instance that is lazily instantiated in draw/fillRoundRect and then
     * subsequently reused to avoid creating a lot of garbage.
     */
    private RoundRectangle2D roundRect;
    
     /**
     * An instance that is lazily instantiated in draw/fillOval and then
     * subsequently reused to avoid creating a lot of garbage.
     */
   private Ellipse2D oval;
    
    /**
     * An instance that is lazily instantiated in draw/fillArc and then
     * subsequently reused to avoid creating a lot of garbage.
     */
    private Arc2D arc;
    
    /** A hidden image used for font metrics. */
    private BufferedImage fmImage;

    /** 
     * A Graphics2D instance for the hidden image that is used for font
     * metrics.  Used in the getFontMetrics(Font f) method.
     */
    private Graphics2D fmImageG2;
    
    /** The FXFontMetrics. */
    private FXFontMetrics fxFontMetrics;

    /** 
     * The device configuration (this is lazily instantiated in the 
     * getDeviceConfiguration() method).
     */
    private GraphicsConfiguration deviceConfiguration;
    
    /**
     * Throws an {@code IllegalArgumentException} if {@code arg} is
     * {@code null}.
     * 
     * @param arg  the argument to check.
     * @param name  the name of the argument (to display in the exception 
     *         message).
     */
    private static void nullNotPermitted(Object arg, String name) {
        if (arg == null) {
            throw new IllegalArgumentException("Null '" + name + "' argument.");
        }    
    }
    
    /**
     * Creates a new instance that will render to the specified JavaFX
     * {@code GraphicsContext}.
     * 
     * @param gc  the graphics context ({@code null} not permitted). 
     */
    public FXGraphics2D(GraphicsContext gc) {
        nullNotPermitted(gc, "gc");
        this.gc = gc;
        this.zeroStrokeWidth = 0.5;
        this.hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, 
                RenderingHints.VALUE_ANTIALIAS_DEFAULT);
        this.hints.put(FXHints.KEY_USE_FX_FONT_METRICS, true);
    }
    
    /**
     * Returns the width to use for the stroke when the AWT stroke
     * specified has a zero width (the default value is {@code 0.5}).  
     * 

In the Java specification for {@code BasicStroke} it states "If width * is set to 0.0f, the stroke is rendered as the thinnest possible * line for the target device and the antialias hint setting." We don't * have a means to implement that accurately since we must specify a fixed * width to the JavaFX canvas - this attribute is the width that is * used.

* * @return The width. */ public double getZeroStrokeWidth() { return this.zeroStrokeWidth; } /** * Sets the width to use for the stroke when setting a new AWT stroke that * has a width of {@code 0.0}. * * @param width the new width (must be 0.0 or greater). */ public void setZeroStrokeWidth(double width) { if (width < 0.0) { throw new IllegalArgumentException("Width cannot be negative."); } this.zeroStrokeWidth = width; } /** * Returns the device configuration associated with this * {@code Graphics2D}. * * @return The device configuration (never {@code null}). */ @Override public GraphicsConfiguration getDeviceConfiguration() { if (this.deviceConfiguration == null) { int width = (int) this.gc.getCanvas().getWidth(); int height = (int) this.gc.getCanvas().getHeight(); this.deviceConfiguration = new FXGraphicsConfiguration(width, height); } return this.deviceConfiguration; } /** * Creates a new graphics object that is a copy of this graphics object. * * @return A new graphics object. */ @Override public Graphics create() { FXGraphics2D copy = new FXGraphics2D(this.gc); copy.setRenderingHints(getRenderingHints()); copy.setClip(getClip()); copy.setPaint(getPaint()); copy.setColor(getColor()); copy.setComposite(getComposite()); copy.setStroke(getStroke()); copy.setFont(getFont()); copy.setTransform(getTransform()); copy.setBackground(getBackground()); return copy; } /** * Returns the paint used to draw or fill shapes (or text). The default * value is {@link Color#BLACK}. This attribute is updated by both the * {@link #setPaint(java.awt.Paint)} and {@link #setColor(java.awt.Color)} * methods. * * @return The paint (never {@code null}). * * @see #setPaint(java.awt.Paint) */ @Override public Paint getPaint() { return this.paint; } /** * Sets the paint used to draw or fill shapes (or text). If * {@code paint} is an instance of {@code Color}, this method will * also update the current color attribute (see {@link #getColor()}). If * you pass {@code null} to this method, it does nothing (in * accordance with the JDK specification). *

* Note that this implementation will map {@link Color}, * {@link GradientPaint}, {@link LinearGradientPaint} and * {@link RadialGradientPaint} to JavaFX equivalents, other paint * implementations are not handled. * * @param paint the paint ({@code null} is permitted but ignored). * * @see #getPaint() */ @Override public void setPaint(Paint paint) { if (paint == null) { return; } if (paintsAreEqual(paint, this.paint)) { return; } applyPaint(paint); } private CycleMethod toJavaFXCycleMethod(MultipleGradientPaint.CycleMethod method) { switch (method) { case NO_CYCLE: return CycleMethod.NO_CYCLE; case REFLECT: return CycleMethod.REFLECT; case REPEAT: return CycleMethod.REPEAT; default: throw new IllegalStateException("Unknown cycle method " + method); } } private void applyPaint(Paint paint) { this.paint = paint; if (paint instanceof Color) { setColor((Color) paint); } else if (paint instanceof GradientPaint) { GradientPaint gp = (GradientPaint) paint; Stop[] stops = new Stop[] { new Stop(0, awtColorToJavaFX(gp.getColor1())), new Stop(1, awtColorToJavaFX(gp.getColor2())) }; Point2D p1 = gp.getPoint1(); Point2D p2 = gp.getPoint2(); CycleMethod cm = gp.isCyclic() ? CycleMethod.REFLECT : CycleMethod.NO_CYCLE; LinearGradient lg = new LinearGradient(p1.getX(), p1.getY(), p2.getX(), p2.getY(), false, cm, stops); this.gc.setStroke(lg); this.gc.setFill(lg); } else if (paint instanceof MultipleGradientPaint) { MultipleGradientPaint mgp = (MultipleGradientPaint) paint; Color[] colors = mgp.getColors(); float[] fractions = mgp.getFractions(); Stop[] stops = new Stop[colors.length]; for (int i = 0; i < colors.length; i++) { stops[i] = new Stop(fractions[i], awtColorToJavaFX(colors[i])); } if (paint instanceof RadialGradientPaint) { RadialGradientPaint rgp = (RadialGradientPaint) paint; Point2D center = rgp.getCenterPoint(); Point2D focus = rgp.getFocusPoint(); double focusDistance = focus.distance(center); double focusAngle = 0.0; if (!focus.equals(center)) { focusAngle = Math.atan2(focus.getY() - center.getY(), focus.getX() - center.getX()); } double radius = rgp.getRadius(); RadialGradient rg = new RadialGradient( Math.toDegrees(focusAngle), focusDistance / radius, center.getX(), center.getY(), radius, false, toJavaFXCycleMethod(rgp.getCycleMethod()), stops); this.gc.setStroke(rg); this.gc.setFill(rg); } else if (paint instanceof LinearGradientPaint) { LinearGradientPaint lgp = (LinearGradientPaint) paint; Point2D start = lgp.getStartPoint(); Point2D end = lgp.getEndPoint(); LinearGradient lg = new LinearGradient(start.getX(), start.getY(), end.getX(), end.getY(), false, toJavaFXCycleMethod(lgp.getCycleMethod()), stops); this.gc.setStroke(lg); this.gc.setFill(lg); } } else { // this is a paint we don't recognise } } /** * Returns the foreground color. This method exists for backwards * compatibility in AWT, you should use the {@link #getPaint()} method. * This attribute is updated by the {@link #setColor(java.awt.Color)} * method, and also by the {@link #setPaint(java.awt.Paint)} method if * a {@code Color} instance is passed to the method. * * @return The foreground color (never {@code null}). * * @see #getPaint() */ @Override public Color getColor() { return this.color; } /** * Sets the foreground color. This method exists for backwards * compatibility in AWT, you should use the * {@link #setPaint(java.awt.Paint)} method. * * @param c the color ({@code null} permitted but ignored). * * @see #setPaint(java.awt.Paint) */ @Override public void setColor(Color c) { if (c == null || c.equals(this.color)) { return; } applyColor(c); } private void applyColor(Color c) { this.color = c; this.paint = c; javafx.scene.paint.Color fxcolor = awtColorToJavaFX(c); this.gc.setFill(fxcolor); this.gc.setStroke(fxcolor); } /** * Returns a JavaFX color that is equivalent to the specified AWT color. * * @param c the color ({@code null} not permitted). * * @return A JavaFX color. */ private javafx.scene.paint.Color awtColorToJavaFX(Color c) { return javafx.scene.paint.Color.rgb(c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha() / 255.0); } /** * Returns the background color (the default value is {@link Color#BLACK}). * This attribute is used by the {@link #clearRect(int, int, int, int)} * method. * * @return The background color (possibly {@code null}). * * @see #setBackground(java.awt.Color) */ @Override public Color getBackground() { return this.background; } /** * Sets the background color. This attribute is used by the * {@link #clearRect(int, int, int, int)} method. The reference * implementation allows {@code null} for the background color, so * we allow that too (but for that case, the {@link #clearRect(int, int, int, int)} * method will do nothing). * * @param color the color ({@code null} permitted). * * @see #getBackground() */ @Override public void setBackground(Color color) { this.background = color; } /** * Returns the current composite. * * @return The current composite (never {@code null}). * * @see #setComposite(java.awt.Composite) */ @Override public Composite getComposite() { return this.composite; } /** * Sets the composite. There is limited handling for * {@code AlphaComposite}, other composites will have no effect on the * output. * * @param comp the composite ({@code null} not permitted). * * @see #getComposite() */ @Override public void setComposite(Composite comp) { nullNotPermitted(comp, "comp"); this.composite = comp; if (comp instanceof AlphaComposite) { AlphaComposite ac = (AlphaComposite) comp; this.gc.setGlobalAlpha(ac.getAlpha()); this.gc.setGlobalBlendMode(blendMode(ac.getRule())); } } /** * Returns a JavaFX BlendMode that is the closest match for the Java2D * alpha composite rule. * * @param rule the rule. * * @return The blend mode. */ private BlendMode blendMode(int rule) { switch (rule) { case AlphaComposite.SRC_ATOP: return BlendMode.SRC_ATOP; case AlphaComposite.CLEAR: case AlphaComposite.DST: case AlphaComposite.DST_ATOP: case AlphaComposite.DST_IN: case AlphaComposite.DST_OUT: case AlphaComposite.DST_OVER: case AlphaComposite.SRC: case AlphaComposite.SRC_IN: case AlphaComposite.SRC_OUT: case AlphaComposite.SRC_OVER: case AlphaComposite.XOR: return BlendMode.SRC_OVER; default: return BlendMode.SRC_OVER; } } /** * Returns the current stroke (this attribute is used when drawing shapes). * * @return The current stroke (never {@code null}). * * @see #setStroke(java.awt.Stroke) */ @Override public Stroke getStroke() { return this.stroke; } /** * Sets the stroke that will be used to draw shapes. * * @param s the stroke ({@code null} not permitted). * * @see #getStroke() */ @Override public void setStroke(Stroke s) { nullNotPermitted(s, "s"); if (s == this.stroke) { // quick test, full equals test later return; } if (stroke instanceof BasicStroke) { BasicStroke bs = (BasicStroke) s; if (bs.equals(this.stroke)) { return; // no change } } this.stroke = s; applyStroke(s); } private void applyStroke(Stroke s) { if (s instanceof BasicStroke) { applyBasicStroke((BasicStroke) s); } } private void applyBasicStroke(BasicStroke bs) { double lineWidth = bs.getLineWidth(); if (lineWidth == 0.0) { lineWidth = this.zeroStrokeWidth; } this.gc.setLineWidth(lineWidth); this.gc.setLineCap(awtToJavaFXLineCap(bs.getEndCap())); this.gc.setLineJoin(awtToJavaFXLineJoin(bs.getLineJoin())); this.gc.setMiterLimit(bs.getMiterLimit()); this.gc.setLineDashes(floatToDoubleArray(bs.getDashArray())); this.gc.setLineDashOffset(bs.getDashPhase()); } /** * Maps a line cap code from AWT to the corresponding JavaFX StrokeLineCap * enum value. * * @param c the line cap code. * * @return A JavaFX line cap value. */ private StrokeLineCap awtToJavaFXLineCap(int c) { if (c == BasicStroke.CAP_BUTT) { return StrokeLineCap.BUTT; } else if (c == BasicStroke.CAP_ROUND) { return StrokeLineCap.ROUND; } else if (c == BasicStroke.CAP_SQUARE) { return StrokeLineCap.SQUARE; } else { throw new IllegalArgumentException("Unrecognised cap code: " + c); } } /** * Maps a line join code from AWT to the corresponding JavaFX * StrokeLineJoin enum value. * * @param j the line join code. * * @return A JavaFX line join value. */ private StrokeLineJoin awtToJavaFXLineJoin(int j) { if (j == BasicStroke.JOIN_BEVEL) { return StrokeLineJoin.BEVEL; } else if (j == BasicStroke.JOIN_MITER) { return StrokeLineJoin.MITER; } else if (j == BasicStroke.JOIN_ROUND) { return StrokeLineJoin.ROUND; } else { throw new IllegalArgumentException("Unrecognised join code: " + j); } } private double[] floatToDoubleArray(float[] f) { if (f == null) { return null; } double[] d = new double[f.length]; for (int i = 0; i < f.length; i++) { d[i] = f[i]; } return d; } /** * Returns the current value for the specified hint. * * @param hintKey the hint key ({@code null} permitted, but the * result will be {@code null} also in that case). * * @return The current value for the specified hint * (possibly {@code null}). * * @see #setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object) */ @Override public Object getRenderingHint(RenderingHints.Key hintKey) { return this.hints.get(hintKey); } /** * Sets the value for a hint. See the {@link FXHints} class for * information about the hints that can be used with this implementation. * * @param hintKey the hint key ({@code null} not permitted). * @param hintValue the hint value. * * @see #getRenderingHint(java.awt.RenderingHints.Key) */ @Override public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { this.hints.put(hintKey, hintValue); } /** * Returns a copy of the rendering hints. Modifying the returned copy * will have no impact on the state of this {@code Graphics2D} * instance. * * @return The rendering hints (never {@code null}). * * @see #setRenderingHints(java.util.Map) */ @Override public RenderingHints getRenderingHints() { return (RenderingHints) this.hints.clone(); } /** * Sets the rendering hints to the specified collection. * * @param hints the new set of hints ({@code null} not permitted). * * @see #getRenderingHints() */ @Override public void setRenderingHints(Map hints) { this.hints.clear(); this.hints.putAll(hints); } /** * Adds all the supplied rendering hints. * * @param hints the hints ({@code null} not permitted). */ @Override public void addRenderingHints(Map hints) { this.hints.putAll(hints); } /** * Draws the specified shape with the current {@code paint} and * {@code stroke}. There is direct handling for {@code Line2D}, * {@code Rectangle2D}, {@code Ellipse2D}, {@code Arc2D} and * {@code Path2D}. All other shapes are mapped to a path outline and then * drawn. * * @param s the shape ({@code null} not permitted). * * @see #fill(java.awt.Shape) */ @Override public void draw(Shape s) { // if the current stroke is not a BasicStroke then it is handled as // a special case if (!(this.stroke instanceof BasicStroke)) { fill(this.stroke.createStrokedShape(s)); return; } if (s instanceof Line2D) { Line2D l = (Line2D) s; Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); if (hint != RenderingHints.VALUE_STROKE_PURE) { double x1 = Math.rint(l.getX1()) - 0.5; double y1 = Math.rint(l.getY1()) - 0.5; double x2 = Math.rint(l.getX2()) - 0.5; double y2 = Math.rint(l.getY2()) - 0.5; l = line(x1, y1, x2, y2); } this.gc.strokeLine(l.getX1(), l.getY1(), l.getX2(), l.getY2()); } else if (s instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; if (s instanceof Rectangle) { r = new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); if (hint != RenderingHints.VALUE_STROKE_PURE) { double x = Math.rint(r.getX()) - 0.5; double y = Math.rint(r.getY()) - 0.5; double w = Math.floor(r.getWidth()); double h = Math.floor(r.getHeight()); r = rect(x, y, w, h); } this.gc.strokeRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } else if (s instanceof RoundRectangle2D) { RoundRectangle2D rr = (RoundRectangle2D) s; this.gc.strokeRoundRect(rr.getX(), rr.getY(), rr.getWidth(), rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); } else if (s instanceof Ellipse2D) { Ellipse2D e = (Ellipse2D) s; this.gc.strokeOval(e.getX(), e.getY(), e.getWidth(), e.getHeight()); } else if (s instanceof Arc2D) { Arc2D a = (Arc2D) s; this.gc.strokeArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), a.getAngleStart(), a.getAngleExtent(), intToArcType(a.getArcType())); } else { shapeToPath(s); this.gc.stroke(); } } private final double[] coords = new double[6]; /** * Maps a shape to a path in the graphics context. * * @param s the shape ({@code null} not permitted). */ private void shapeToPath(Shape s) { if (s instanceof Path2D) { Path2D path = (Path2D) s; if (path.getWindingRule() == Path2D.WIND_EVEN_ODD) { this.gc.setFillRule(FillRule.EVEN_ODD); } else { this.gc.setFillRule(FillRule.NON_ZERO); } } this.gc.beginPath(); PathIterator iterator = s.getPathIterator(null); while (!iterator.isDone()) { int segType = iterator.currentSegment(coords); switch (segType) { case PathIterator.SEG_MOVETO: this.gc.moveTo(coords[0], coords[1]); break; case PathIterator.SEG_LINETO: this.gc.lineTo(coords[0], coords[1]); break; case PathIterator.SEG_QUADTO: this.gc.quadraticCurveTo(coords[0], coords[1], coords[2], coords[3]); break; case PathIterator.SEG_CUBICTO: this.gc.bezierCurveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); break; case PathIterator.SEG_CLOSE: this.gc.closePath(); break; default: throw new RuntimeException("Unrecognised segment type " + segType); } iterator.next(); } } private ArcType intToArcType(int t) { if (t == Arc2D.CHORD) { return ArcType.CHORD; } else if (t == Arc2D.OPEN) { return ArcType.OPEN; } else if (t == Arc2D.PIE) { return ArcType.ROUND; } throw new IllegalArgumentException("Unrecognised t: " + t); } /** * Fills the specified shape with the current {@code paint}. There is * direct handling for {@code RoundRectangle2D}, * {@code Rectangle2D}, {@code Ellipse2D} and {@code Arc2D}. * All other shapes are mapped to a path outline and then filled. * * @param s the shape ({@code null} not permitted). * * @see #draw(java.awt.Shape) */ @Override public void fill(Shape s) { if (s instanceof Rectangle2D) { Rectangle2D r = (Rectangle2D) s; this.gc.fillRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } else if (s instanceof RoundRectangle2D) { RoundRectangle2D rr = (RoundRectangle2D) s; this.gc.fillRoundRect(rr.getX(), rr.getY(), rr.getWidth(), rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); } else if (s instanceof Ellipse2D) { Ellipse2D e = (Ellipse2D) s; this.gc.fillOval(e.getX(), e.getY(), e.getWidth(), e.getHeight()); } else if (s instanceof Arc2D) { Arc2D a = (Arc2D) s; this.gc.fillArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), a.getAngleStart(), a.getAngleExtent(), intToArcType(a.getArcType())); } else { shapeToPath(s); this.gc.fill(); } } /** * Returns the current font used for drawing text. * * @return The current font (never {@code null}). * * @see #setFont(java.awt.Font) */ @Override public Font getFont() { return this.font; } /** * Sets the font to be used for drawing text. * * @param font the font ({@code null} is permitted but ignored). * * @see #getFont() */ @Override public void setFont(Font font) { if (font == null || this.font.equals(font)) { return; } applyFont(font); } private void applyFont(Font font) { this.font = font; FontWeight weight = font.isBold() ? FontWeight.BOLD : FontWeight.NORMAL; FontPosture posture = font.isItalic() ? FontPosture.ITALIC : FontPosture.REGULAR; javafx.scene.text.Font jfxfont = javafx.scene.text.Font.font( font.getFamily(), weight, posture, font.getSize()); this.gc.setFont(jfxfont); } /** * Returns the font metrics for the specified font. The font metrics * returned are from Java2D (via an internal {@code BufferedImage}) which * does not always match exactly the font metrics used by JavaFX. * * @param f the font. * * @return The font metrics. */ @Override public FontMetrics getFontMetrics(Font f) { if (getRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS) == Boolean.TRUE) { if (this.fxFontMetrics == null || !f.equals(this.fxFontMetrics.getFont())) { this.fxFontMetrics = new FXFontMetrics(this.font, this); } return this.fxFontMetrics; } // be lazy about creating the underlying objects... if (this.fmImage == null) { this.fmImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB); this.fmImageG2 = this.fmImage.createGraphics(); this.fmImageG2.setRenderingHint( RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); } return this.fmImageG2.getFontMetrics(f); } /** * Returns the font render context. The implementation here returns the * {@code FontRenderContext} for an image that is maintained * internally (as for {@link #getFontMetrics}). * * @return The font render context. */ @Override public FontRenderContext getFontRenderContext() { return this.fontRenderContext; } /** * Draws a string at {@code (x, y)}. The start of the text at the * baseline level will be aligned with the {@code (x, y)} point. * * @param str the string ({@code null} not permitted). * @param x the x-coordinate. * @param y the y-coordinate. * * @see #drawString(java.lang.String, float, float) */ @Override public void drawString(String str, int x, int y) { drawString(str, (float) x, (float) y); } /** * Draws a string at {@code (x, y)}. The start of the text at the * baseline level will be aligned with the {@code (x, y)} point. * * @param str the string ({@code null} not permitted). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawString(String str, float x, float y) { if (str == null) { throw new NullPointerException("Null 'str' argument."); } this.gc.fillText(str, x, y); } /** * Draws a string of attributed characters at {@code (x, y)}. The * call is delegated to * {@link #drawString(AttributedCharacterIterator, float, float)}. * * @param iterator an iterator for the characters. * @param x the x-coordinate. * @param y the x-coordinate. */ @Override public void drawString(AttributedCharacterIterator iterator, int x, int y) { drawString(iterator, (float) x, (float) y); } /** * Draws a string of attributed characters at {@code (x, y)}. * * @param iterator an iterator over the characters ({@code null} not * permitted). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawString(AttributedCharacterIterator iterator, float x, float y) { Set s = iterator.getAllAttributeKeys(); if (!s.isEmpty()) { TextLayout layout = new TextLayout(iterator, getFontRenderContext()); layout.draw(this, x, y); } else { StringBuilder strb = new StringBuilder(); iterator.first(); for (int i = iterator.getBeginIndex(); i < iterator.getEndIndex(); i++) { strb.append(iterator.current()); iterator.next(); } drawString(strb.toString(), x, y); } } /** * Draws the specified glyph vector at the location {@code (x, y)}. * * @param g the glyph vector ({@code null} not permitted). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawGlyphVector(GlyphVector g, float x, float y) { fill(g.getOutline(x, y)); } /** * Applies the translation {@code (tx, ty)}. This call is delegated * to {@link #translate(double, double)}. * * @param tx the x-translation. * @param ty the y-translation. * * @see #translate(double, double) */ @Override public void translate(int tx, int ty) { translate(tx, (double) ty); } /** * Applies the translation {@code (tx, ty)}. * * @param tx the x-translation. * @param ty the y-translation. */ @Override public void translate(double tx, double ty) { this.transform.translate(tx, ty); this.gc.translate(tx, ty); } /** * Applies a rotation (anti-clockwise) about {@code (0, 0)}. * * @param theta the rotation angle (in radians). */ @Override public void rotate(double theta) { this.transform.rotate(theta); this.gc.rotate(Math.toDegrees(theta)); } /** * Applies a rotation (anti-clockwise) about {@code (x, y)}. * * @param theta the rotation angle (in radians). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void rotate(double theta, double x, double y) { translate(x, y); rotate(theta); translate(-x, -y); } /** * Applies a scale transformation. * * @param sx the x-scaling factor. * @param sy the y-scaling factor. */ @Override public void scale(double sx, double sy) { this.transform.scale(sx, sy); this.gc.scale(sx, sy); } /** * Applies a shear transformation. This is equivalent to the following * call to the {@code transform} method: *

*
  • * {@code transform(AffineTransform.getShearInstance(shx, shy));} *
* * @param shx the x-shear factor. * @param shy the y-shear factor. */ @Override public void shear(double shx, double shy) { transform(AffineTransform.getShearInstance(shx, shy)); } /** * Applies this transform to the existing transform by concatenating it. * * @param t the transform ({@code null} not permitted). */ @Override public void transform(AffineTransform t) { AffineTransform tx = getTransform(); tx.concatenate(t); setTransform(tx); } /** * Returns a copy of the current transform. * * @return A copy of the current transform (never {@code null}). * * @see #setTransform(java.awt.geom.AffineTransform) */ @Override public AffineTransform getTransform() { return (AffineTransform) this.transform.clone(); } /** * Sets the transform. * * @param t the new transform ({@code null} permitted, resets to the * identity transform). * * @see #getTransform() */ @Override public void setTransform(AffineTransform t) { if (t == null) { this.transform = new AffineTransform(); t = this.transform; } else { this.transform = new AffineTransform(t); } this.gc.setTransform(t.getScaleX(), t.getShearY(), t.getShearX(), t.getScaleY(), t.getTranslateX(), t.getTranslateY()); } /** * Returns {@code true} if the rectangle (in device space) intersects * with the shape (the interior, if {@code onStroke} is {@code false}, * otherwise the stroked outline of the shape). * * @param rect a rectangle (in device space). * @param s the shape. * @param onStroke test the stroked outline only? * * @return A boolean. */ @Override public boolean hit(Rectangle rect, Shape s, boolean onStroke) { Shape ts; if (onStroke) { ts = this.transform.createTransformedShape( this.stroke.createStrokedShape(s)); } else { ts = this.transform.createTransformedShape(s); } if (!rect.getBounds2D().intersects(ts.getBounds2D())) { return false; } Area a1 = new Area(rect); Area a2 = new Area(ts); a1.intersect(a2); return !a1.isEmpty(); } /** * Not implemented - the method does nothing. */ @Override public void setPaintMode() { // not implemented } /** * Not implemented - the method does nothing. */ @Override public void setXORMode(Color c1) { // not implemented } /** * Returns the bounds of the user clipping region. * * @return The clip bounds (possibly {@code null}). * * @see #getClip() */ @Override public Rectangle getClipBounds() { if (this.clip == null) { return null; } return getClip().getBounds(); } /** * Returns the user clipping region. The initial default value is * {@code null}. * * @return The user clipping region (possibly {@code null}). * * @see #setClip(java.awt.Shape) */ @Override public Shape getClip() { if (this.clip == null) { return null; } AffineTransform inv; try { inv = this.transform.createInverse(); return inv.createTransformedShape(this.clip); } catch (NoninvertibleTransformException ex) { return null; } } /** * Sets the user clipping region. * * @param shape the new user clipping region ({@code null} permitted). * * @see #getClip() */ @Override public void setClip(Shape shape) { if (this.stateSaved) { this.gc.restore(); // get back original clip reapplyAttributes(); // but keep other attributes this.stateSaved = false; } // null is handled fine here... this.clip = this.transform.createTransformedShape(shape); if (clip != null) { this.gc.save(); rememberSavedAttributes(); shapeToPath(shape); this.gc.clip(); } } /** * Remember the Graphics2D attributes in force at the point of pushing * the JavaFX context. */ private void rememberSavedAttributes() { this.stateSaved = true; this.savedColor = this.color; this.savedFont = this.font; this.savedPaint = this.paint; this.savedStroke = this.stroke; this.savedTransform = new AffineTransform(this.transform); } private void reapplyAttributes() { if (!paintsAreEqual(this.paint, this.savedPaint)) { applyPaint(this.paint); } if (!this.color.equals(this.savedColor)) { applyColor(this.color); } if (!this.stroke.equals(this.savedStroke)) { applyStroke(this.stroke); } if (!this.font.equals(this.savedFont)) { applyFont(this.font); } if (!this.transform.equals(this.savedTransform)) { setTransform(this.transform); } this.savedColor = null; this.savedFont = null; this.savedPaint = null; this.savedStroke = null; this.savedTransform = null; } /** * Clips to the intersection of the current clipping region and the * specified shape. * * According to the Oracle API specification, this method will accept a * {@code null} argument, but there is an open bug report (since 2004) * that suggests this is wrong: *

* * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189 * * In this implementation, a {@code null} argument is not permitted. * * @param s the clip shape ({@code null} not permitted). */ @Override public void clip(Shape s) { if (s instanceof Line2D) { s = s.getBounds2D(); } if (this.clip == null) { setClip(s); return; } Shape ts = this.transform.createTransformedShape(s); Shape clipNew; if (!ts.intersects(this.clip.getBounds2D())) { clipNew = new Rectangle2D.Double(); } else { Area a1 = new Area(ts); Area a2 = new Area(this.clip); a1.intersect(a2); clipNew = new Path2D.Double(a1); } this.clip = clipNew; if (!this.stateSaved) { this.gc.save(); rememberSavedAttributes(); } shapeToPath(this.clip); this.gc.clip(); } /** * Clips to the intersection of the current clipping region and the * specified rectangle. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. */ @Override public void clipRect(int x, int y, int width, int height) { clip(rect(x, y, width, height)); } /** * Sets the user clipping region to the specified rectangle. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #getClip() */ @Override public void setClip(int x, int y, int width, int height) { setClip(rect(x, y, width, height)); } /** * Draws a line from {@code (x1, y1)} to {@code (x2, y2)} using * the current {@code paint} and {@code stroke}. * * @param x1 the x-coordinate of the start point. * @param y1 the y-coordinate of the start point. * @param x2 the x-coordinate of the end point. * @param y2 the x-coordinate of the end point. */ @Override public void drawLine(int x1, int y1, int x2, int y2) { draw(line(x1, y1, x2, y2)); } /** * Fills the specified rectangle with the current {@code paint}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the rectangle width. * @param height the rectangle height. */ @Override public void fillRect(int x, int y, int width, int height) { fill(rect(x, y, width, height)); } /** * Clears the specified rectangle by filling it with the current * background color. If the background color is {@code null}, this * method will do nothing. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #getBackground() */ @Override public void clearRect(int x, int y, int width, int height) { if (getBackground() == null) { return; // we can't do anything } Paint saved = getPaint(); setPaint(getBackground()); fillRect(x, y, width, height); setPaint(saved); } /** * Draws a rectangle with rounded corners using the current * {@code paint} and {@code stroke}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param arcWidth the arc-width. * @param arcHeight the arc-height. * * @see #fillRoundRect(int, int, int, int, int, int) */ @Override public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { draw(roundRect(x, y, width, height, arcWidth, arcHeight)); } /** * Fills a rectangle with rounded corners using the current {@code paint}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param arcWidth the arc-width. * @param arcHeight the arc-height. * * @see #drawRoundRect(int, int, int, int, int, int) */ @Override public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { fill(roundRect(x, y, width, height, arcWidth, arcHeight)); } /** * Draws an oval framed by the rectangle {@code (x, y, width, height)} * using the current {@code paint} and {@code stroke}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #fillOval(int, int, int, int) */ @Override public void drawOval(int x, int y, int width, int height) { draw(oval(x, y, width, height)); } /** * Fills an oval framed by the rectangle {@code (x, y, width, height)}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @see #drawOval(int, int, int, int) */ @Override public void fillOval(int x, int y, int width, int height) { fill(oval(x, y, width, height)); } /** * Draws an arc contained within the rectangle * {@code (x, y, width, height)}, starting at {@code startAngle} * and continuing through {@code arcAngle} degrees using * the current {@code paint} and {@code stroke}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param startAngle the start angle in degrees, 0 = 3 o'clock. * @param arcAngle the angle (anticlockwise) in degrees. * * @see #fillArc(int, int, int, int, int, int) */ @Override public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { draw(arc(x, y, width, height, startAngle, arcAngle)); } /** * Fills an arc contained within the rectangle * {@code (x, y, width, height)}, starting at {@code startAngle} * and continuing through {@code arcAngle} degrees, using * the current {@code paint}. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param startAngle the start angle in degrees, 0 = 3 o'clock. * @param arcAngle the angle (anticlockwise) in degrees. * * @see #drawArc(int, int, int, int, int, int) */ @Override public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { fill(arc(x, y, width, height, startAngle, arcAngle)); } /** * Draws the specified multi-segment line using the current * {@code paint} and {@code stroke}. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polyline. */ @Override public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { GeneralPath p = createPolygon(xPoints, yPoints, nPoints, false); draw(p); } /** * Draws the specified polygon using the current {@code paint} and * {@code stroke}. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polygon. * * @see #fillPolygon(int[], int[], int) */ @Override public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { GeneralPath p = createPolygon(xPoints, yPoints, nPoints, true); draw(p); } /** * Fills the specified polygon using the current {@code paint}. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polygon. * * @see #drawPolygon(int[], int[], int) */ @Override public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { GeneralPath p = createPolygon(xPoints, yPoints, nPoints, true); fill(p); } /** * Creates a polygon from the specified {@code x} and * {@code y} coordinate arrays. * * @param xPoints the x-points. * @param yPoints the y-points. * @param nPoints the number of points to use for the polyline. * @param close closed? * * @return A polygon. */ public GeneralPath createPolygon(int[] xPoints, int[] yPoints, int nPoints, boolean close) { GeneralPath p = new GeneralPath(); p.moveTo(xPoints[0], yPoints[0]); for (int i = 1; i < nPoints; i++) { p.lineTo(xPoints[i], yPoints[i]); } if (close) { p.closePath(); } return p; } /** * Draws an image at the location {@code (x, y)}. Note that the * {@code observer} is ignored. * * @param img the image ({@code null} permitted...method will do nothing). * @param x the x-coordinate. * @param y the y-coordinate. * @param observer ignored. * * @return {@code true} if there is no more drawing to be done. */ @Override public boolean drawImage(Image img, int x, int y, ImageObserver observer) { if (img == null) { return true; } int w = img.getWidth(observer); if (w < 0) { return false; } int h = img.getHeight(observer); if (h < 0) { return false; } return drawImage(img, x, y, w, h, observer); } /** * Draws the image into the rectangle defined by {@code (x, y, w, h)}. * Note that the {@code observer} is ignored (it is not useful in this * context). * * @param img the image ({@code null} permitted...draws nothing). * @param x the x-coordinate. * @param y the y-coordinate. * @param w the width. * @param h the height. * @param observer ignored. * * @return {@code true} if there is no more drawing to be done. */ @Override public boolean drawImage(final Image img, int x, int y, int w, int h, ImageObserver observer) { final BufferedImage buffered; if (img instanceof BufferedImage) { buffered = (BufferedImage) img; } else { buffered = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); final Graphics2D g2 = buffered.createGraphics(); g2.drawImage(img, 0, 0, w, h, null); g2.dispose(); } javafx.scene.image.WritableImage fxImage = SwingFXUtils.toFXImage( buffered, null); this.gc.drawImage(fxImage, x, y, w, h); return true; } /** * Draws an image at the location {@code (x, y)}. Note that the * {@code observer} is ignored. * * @param img the image ({@code null} permitted...draws nothing). * @param x the x-coordinate. * @param y the y-coordinate. * @param bgcolor the background color ({@code null} permitted). * @param observer ignored. * * @return {@code true} if there is no more drawing to be done. */ @Override public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { if (img == null) { return true; } int w = img.getWidth(null); if (w < 0) { return false; } int h = img.getHeight(null); if (h < 0) { return false; } return drawImage(img, x, y, w, h, bgcolor, observer); } /** * Draws an image to the rectangle {@code (x, y, w, h)} (scaling it if * required), first filling the background with the specified color. Note * that the {@code observer} is ignored. * * @param img the image. * @param x the x-coordinate. * @param y the y-coordinate. * @param w the width. * @param h the height. * @param bgcolor the background color ({@code null} permitted). * @param observer ignored. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int x, int y, int w, int h, Color bgcolor, ImageObserver observer) { Paint saved = getPaint(); setPaint(bgcolor); fillRect(x, y, w, h); setPaint(saved); return drawImage(img, x, y, w, h, observer); } /** * Draws part of an image (defined by the source rectangle * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle * {@code (dx1, dy1, dx2, dy2)}. Note that the {@code observer} * is ignored in this implementation. * * @param img the image. * @param dx1 the x-coordinate for the top left of the destination. * @param dy1 the y-coordinate for the top left of the destination. * @param dx2 the x-coordinate for the bottom right of the destination. * @param dy2 the y-coordinate for the bottom right of the destination. * @param sx1 the x-coordinate for the top left of the source. * @param sy1 the y-coordinate for the top left of the source. * @param sx2 the x-coordinate for the bottom right of the source. * @param sy2 the y-coordinate for the bottom right of the source. * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { int w = dx2 - dx1; int h = dy2 - dy1; BufferedImage img2 = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = img2.createGraphics(); g2.drawImage(img, 0, 0, w, h, sx1, sy1, sx2, sy2, null); return drawImage(img2, dx1, dy1, null); } /** * Draws part of an image (defined by the source rectangle * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle * {@code (dx1, dy1, dx2, dy2)}. The destination rectangle is first * cleared by filling it with the specified {@code bgcolor}. Note that * the {@code observer} is ignored. * * @param img the image. * @param dx1 the x-coordinate for the top left of the destination. * @param dy1 the y-coordinate for the top left of the destination. * @param dx2 the x-coordinate for the bottom right of the destination. * @param dy2 the y-coordinate for the bottom right of the destination. * @param sx1 the x-coordinate for the top left of the source. * @param sy1 the y-coordinate for the top left of the source. * @param sx2 the x-coordinate for the bottom right of the source. * @param sy2 the y-coordinate for the bottom right of the source. * @param bgcolor the background color ({@code null} permitted). * @param observer ignored. * * @return {@code true} if the image is drawn. */ @Override 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) { Paint saved = getPaint(); setPaint(bgcolor); fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1); setPaint(saved); return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); } /** * Draws the rendered image. When {@code img} is {@code null} this method * does nothing. * * @param img the image ({@code null} permitted). * @param xform the transform. */ @Override public void drawRenderedImage(RenderedImage img, AffineTransform xform) { if (img == null) { // to match the behaviour specified in the JDK return; } BufferedImage bi = convertRenderedImage(img); drawImage(bi, xform, null); } /** * Converts a rendered image to a {@code BufferedImage}. This utility * method has come from a forum post by Jim Moore at: *

* * http://www.jguru.com/faq/view.jsp?EID=114602 * * @param img the rendered image. * * @return A buffered image. */ private static BufferedImage convertRenderedImage(RenderedImage img) { if (img instanceof BufferedImage) { return (BufferedImage) img; } ColorModel cm = img.getColorModel(); int width = img.getWidth(); int height = img.getHeight(); WritableRaster raster = cm.createCompatibleWritableRaster(width, height); boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); Hashtable properties = new Hashtable(); String[] keys = img.getPropertyNames(); if (keys != null) { for (String key : keys) { properties.put(key, img.getProperty(key)); } } BufferedImage result = new BufferedImage(cm, raster, isAlphaPremultiplied, properties); img.copyData(raster); return result; } /** * Draws the renderable image. * * @param img the renderable image. * @param xform the transform. */ @Override public void drawRenderableImage(RenderableImage img, AffineTransform xform) { RenderedImage ri = img.createDefaultRendering(); drawRenderedImage(ri, xform); } /** * Draws an image with the specified transform. Note that the * {@code observer} is ignored in this implementation. * * @param img the image. * @param xform the transform ({@code null} permitted). * @param obs the image observer (ignored). * * @return {@code true} if the image is drawn. */ @Override public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { AffineTransform savedTransform = getTransform(); if (xform != null) { transform(xform); } boolean result = drawImage(img, 0, 0, obs); if (xform != null) { setTransform(savedTransform); } return result; } /** * Draws the image resulting from applying the {@code BufferedImageOp} * to the specified image at the location {@code (x, y)}. * * @param img the image. * @param op the operation ({@code null} permitted). * @param x the x-coordinate. * @param y the y-coordinate. */ @Override public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { BufferedImage imageToDraw = img; if (op != null) { imageToDraw = op.filter(img, null); } drawImage(imageToDraw, new AffineTransform(1f, 0f, 0f, 1f, x, y), null); } /** * Not yet implemented. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width of the area. * @param height the height of the area. * @param dx the delta x. * @param dy the delta y. */ @Override public void copyArea(int x, int y, int width, int height, int dx, int dy) { // FIXME: implement this, low priority } /** * This method does nothing. */ @Override public void dispose() { // nothing to do } /** * Returns a recyclable {@link Line2D} object. * * @param x1 the x-coordinate. * @param y1 the y-coordinate. * @param x2 the width. * @param y2 the height. * * @return A line (never {@code null}). */ private Line2D line(double x1, double y1, double x2, double y2) { if (this.line == null) { this.line = new Line2D.Double(x1, y1, x2, y2); } else { this.line.setLine(x1, y1, x2, y2); } return this.line; } /** * Sets the attributes of the reusable {@link Rectangle2D} object that is * used by the {@link FXGraphics2D#drawRect(int, int, int, int)} and * {@link FXGraphics2D#fillRect(int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @return A rectangle (never {@code null}). */ private Rectangle2D rect(double x, double y, double width, double height) { if (this.rect == null) { this.rect = new Rectangle2D.Double(x, y, width, height); } else { this.rect.setRect(x, y, width, height); } return this.rect; } /** * Sets the attributes of the reusable {@link RoundRectangle2D} object that * is used by the {@link #drawRoundRect(int, int, int, int, int, int)} and * {@link #fillRoundRect(int, int, int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param arcWidth the arc width. * @param arcHeight the arc height. * * @return A round rectangle (never {@code null}). */ private RoundRectangle2D roundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { if (this.roundRect == null) { this.roundRect = new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight); } else { this.roundRect.setRoundRect(x, y, width, height, arcWidth, arcHeight); } return this.roundRect; } /** * Sets the attributes of the reusable {@link Arc2D} object that is used by * {@link #drawArc(int, int, int, int, int, int)} and * {@link #fillArc(int, int, int, int, int, int)} methods. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * @param startAngle the start angle in degrees, 0 = 3 o'clock. * @param arcAngle the angle (anticlockwise) in degrees. * * @return An arc (never {@code null}). */ private Arc2D arc(int x, int y, int width, int height, int startAngle, int arcAngle) { if (this.arc == null) { this.arc = new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN); } else { this.arc.setArc(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN); } return this.arc; } /** * Returns an {@link Ellipse2D} object that may be reused (so this instance * should be used for short term operations only). See the * {@link #drawOval(int, int, int, int)} and * {@link #fillOval(int, int, int, int)} methods for usage. * * @param x the x-coordinate. * @param y the y-coordinate. * @param width the width. * @param height the height. * * @return An oval shape (never {@code null}). */ private Ellipse2D oval(int x, int y, int width, int height) { if (this.oval == null) { this.oval = new Ellipse2D.Double(x, y, width, height); } else { this.oval.setFrame(x, y, width, height); } return this.oval; } /** * Returns {@code true} if the two {@code Paint} objects are equal * OR both {@code null}. This method handles * {@code GradientPaint}, {@code LinearGradientPaint} * and {@code RadialGradientPaint} as special cases, since those classes do * not override the {@code equals()} method. * * @param p1 paint 1 ({@code null} permitted). * @param p2 paint 2 ({@code null} permitted). * * @return A boolean. */ private static boolean paintsAreEqual(Paint p1, Paint p2) { if (p1 == p2) { return true; } // handle cases where either or both arguments are null if (p1 == null) { return (p2 == null); } if (p2 == null) { return false; } // handle cases... if (p1 instanceof Color && p2 instanceof Color) { return p1.equals(p2); } if (p1 instanceof GradientPaint && p2 instanceof GradientPaint) { GradientPaint gp1 = (GradientPaint) p1; GradientPaint gp2 = (GradientPaint) p2; return gp1.getColor1().equals(gp2.getColor1()) && gp1.getColor2().equals(gp2.getColor2()) && gp1.getPoint1().equals(gp2.getPoint1()) && gp1.getPoint2().equals(gp2.getPoint2()) && gp1.isCyclic() == gp2.isCyclic() && gp1.getTransparency() == gp1.getTransparency(); } if (p1 instanceof LinearGradientPaint && p2 instanceof LinearGradientPaint) { LinearGradientPaint lgp1 = (LinearGradientPaint) p1; LinearGradientPaint lgp2 = (LinearGradientPaint) p2; return lgp1.getStartPoint().equals(lgp2.getStartPoint()) && lgp1.getEndPoint().equals(lgp2.getEndPoint()) && Arrays.equals(lgp1.getFractions(), lgp2.getFractions()) && Arrays.equals(lgp1.getColors(), lgp2.getColors()) && lgp1.getCycleMethod() == lgp2.getCycleMethod() && lgp1.getColorSpace() == lgp2.getColorSpace() && lgp1.getTransform().equals(lgp2.getTransform()); } if (p1 instanceof RadialGradientPaint && p2 instanceof RadialGradientPaint) { RadialGradientPaint rgp1 = (RadialGradientPaint) p1; RadialGradientPaint rgp2 = (RadialGradientPaint) p2; return rgp1.getCenterPoint().equals(rgp2.getCenterPoint()) && rgp1.getRadius() == rgp2.getRadius() && rgp1.getFocusPoint().equals(rgp2.getFocusPoint()) && Arrays.equals(rgp1.getFractions(), rgp2.getFractions()) && Arrays.equals(rgp1.getColors(), rgp2.getColors()) && rgp1.getCycleMethod() == rgp2.getCycleMethod() && rgp1.getColorSpace() == rgp2.getColorSpace() && rgp1.getTransform().equals(rgp2.getTransform()); } return p1.equals(p2); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy