GefDiagramExportService.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ge.gef.ui.services;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import javax.imageio.ImageIO;
import javax.swing.JTextArea;

import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.emf.common.util.URI;
import org.eclipse.gef.fx.anchors.IAnchor;
import org.eclipse.gef.fx.nodes.Connection;
import org.osate.ge.GraphicalEditor;
import org.osate.ge.ProjectUtil;
import org.osate.ge.gef.AgeGefRuntimeException;
import org.osate.ge.gef.BaseConnectionNode;
import org.osate.ge.gef.ui.diagram.GefAgeDiagram;
import org.osate.ge.gef.ui.editor.AgeEditor;
import org.osate.ge.graphics.Dimension;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DiagramSerialization;
import org.osate.ge.internal.diagram.runtime.layout.DiagramElementLayoutUtil;
import org.osate.ge.internal.diagram.runtime.updating.BusinessObjectNodeFactory;
import org.osate.ge.internal.diagram.runtime.updating.DefaultBusinessObjectTreeUpdater;
import org.osate.ge.internal.diagram.runtime.updating.DefaultDiagramElementGraphicalConfigurationProvider;
import org.osate.ge.internal.diagram.runtime.updating.DiagramUpdater;
import org.osate.ge.internal.services.ActionService;
import org.osate.ge.internal.services.ExtensionRegistryService;
import org.osate.ge.internal.services.InternalDiagramExportService;
import org.osate.ge.internal.services.ProjectProvider;
import org.osate.ge.internal.services.ProjectReferenceService;
import org.osate.ge.internal.services.ReferenceService;
import org.osate.ge.internal.services.impl.DefaultColoringService;
import org.osate.ge.internal.services.impl.ProjectReferenceServiceProxy;
import org.osate.ge.services.QueryService;
import org.osate.ge.services.impl.DefaultQueryService;
import org.osgi.framework.FrameworkUtil;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;

import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Region;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.Circle;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.QuadCurveTo;
import javafx.scene.shape.VLineTo;
import javafx.scene.text.Text;
import javafx.scene.transform.Transform;

/**
 * Diagram export service implementation for the GEF implementation of the graphical editor.
 * Generates an image based on the JavaFX scene graph. Only a subset of JavaFX scene graph nodes are supported.
 * To properly export diagrams, this class must be updated to support any JavaFX shapes used in the scene graph.
 * See {@link #isExportNode(Node)}.
 *
 */
public class GefDiagramExportService implements InternalDiagramExportService {
	// Extra padding to account for rendering to rater image not supporting "inside" lines.
	private static final int BOUNDS_PADDING = 2;
	private static final java.awt.Color TRANSPARENT = new java.awt.Color(0, 0, 0, 0);

	@Override
	public void export(final IFile diagramFile, final OutputStream outputStream, final String format, double scaling)
			throws IOException {
		try (GefAgeDiagram diagram = loadDiagram(diagramFile)) {
			export(diagram, diagram.getDiagram(), diagram.getSceneNode().getSceneToLocalTransform(), scaling,
					outputStream, format);
		}
	}

	@Override
	public void export(final GraphicalEditor editor, final OutputStream outputStream, final String format,
			final DiagramNode exportNode, final double scaling) throws IOException {
		Objects.requireNonNull(exportNode, "exportNode must not be null");
		final GefAgeDiagram diagram = checkEditor(editor).getGefDiagram();
		export(diagram, exportNode, diagram.getSceneNode().getSceneToLocalTransform(), scaling, outputStream, format);
	}

	@Override
	public BufferedImage export(final GraphicalEditor editor, final DiagramNode exportNode, final double scaling) {
		Objects.requireNonNull(exportNode, "exportNode must not be null");
		final GefAgeDiagram diagram = checkEditor(editor).getGefDiagram();
		return exportToRasterImage(diagram, exportNode, diagram.getSceneNode().getSceneToLocalTransform(), scaling);
	}

	private static void export(final GefAgeDiagram diagram, final DiagramNode exportRootDiagramNode,
			final Transform sceneToDiagramTransform, final double scaling, final OutputStream output,
			final String format) throws IOException {
		if ("svg".equalsIgnoreCase(format)) {
			final Writer outWriter = new OutputStreamWriter(output, StandardCharsets.UTF_8);
			exportToSvg(diagram, exportRootDiagramNode, sceneToDiagramTransform, scaling).stream(outWriter, false);
		} else {
			final BufferedImage image = exportToRasterImage(diagram, exportRootDiagramNode, sceneToDiagramTransform,
					scaling);
			ImageIO.write(image, format, output);
		}
	}

	private static SVGGraphics2D exportToSvg(final GefAgeDiagram diagram, final DiagramNode exportRootDiagramNode,
			final Transform sceneToDiagramTransform, final double scaling) {
		final List<Node> exportNodes = getExportNodes(diagram, exportRootDiagramNode);
		final Bounds bounds = getBounds(exportNodes, sceneToDiagramTransform);
		final Transform sceneToExportTransform = Transform.translate(-bounds.getMinX(), -bounds.getMinY())
				.createConcatenation(sceneToDiagramTransform);

		// Generate the SVG
		DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
		String svgNS = "http://www.w3.org/2000/svg";
		Document doc = domImpl.createDocument(svgNS, "svg", null);
		final SVGGraphics2D svgGenerator = new SVGGraphics2D(doc);
		svgGenerator.setSVGCanvasSize(new java.awt.Dimension((int) Math.ceil(bounds.getWidth() * scaling),
				(int) Math.ceil(bounds.getHeight() * scaling)));
		draw(exportNodes, sceneToExportTransform, scaling, svgGenerator);

		return svgGenerator;
	}

	private static BufferedImage exportToRasterImage(final GefAgeDiagram diagram,
			final DiagramNode exportRootDiagramNode, final Transform sceneToDiagramTransform, final double scaling) {
		final List<Node> exportNodes = getExportNodes(diagram, exportRootDiagramNode);
		final Bounds bounds = getBounds(exportNodes, sceneToDiagramTransform);
		final Transform sceneToExportTransform = Transform.translate(-bounds.getMinX(), -bounds.getMinY())
				.createConcatenation(sceneToDiagramTransform);

		final BufferedImage image = new BufferedImage((int) Math.ceil(bounds.getWidth() * scaling),
				(int) Math.ceil(bounds.getHeight() * scaling), BufferedImage.TYPE_INT_RGB);
		final Graphics2D graphics = image.createGraphics();
		graphics.setColor(new Color(255, 255, 255, 255));
		graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
		draw(exportNodes, sceneToExportTransform, scaling, graphics);
		return image;
	}

	/**
	 * Returns the bounds of the specified nodes in diagram coordinates.
	 * @param nodes the nodes for which to get the bounds.
	 * @param sceneToDiagramTransform the transformation from scene to diagram coordinates.
	 * @return the bounds in diagram coordinates.
	 */
	private static Bounds getBounds(final List<Node> nodes, final Transform sceneToDiagramTransform) {
		double minX = Double.POSITIVE_INFINITY;
		double minY = Double.POSITIVE_INFINITY;
		double maxX = Double.NEGATIVE_INFINITY;
		double maxY = Double.NEGATIVE_INFINITY;

		for (final Node node : nodes) {
			final Bounds nodeBoundsInDiagram = sceneToDiagramTransform
					.transform(node.getLocalToSceneTransform().transform(node.getBoundsInLocal()));
			minX = Math.min(minX, nodeBoundsInDiagram.getMinX());
			minY = Math.min(minY, nodeBoundsInDiagram.getMinY());
			maxX = Math.max(maxX, nodeBoundsInDiagram.getMaxX());
			maxY = Math.max(maxY, nodeBoundsInDiagram.getMaxY());
		}

		return new BoundingBox(minX - BOUNDS_PADDING, minY - BOUNDS_PADDING, maxX - minX + 2 * BOUNDS_PADDING,
				maxY - minY + 2 * BOUNDS_PADDING);
	}

	@Override
	public Dimension getDimensions(final GraphicalEditor editor, final DiagramNode exportNode) {
		Objects.requireNonNull(exportNode, "exportNode must not be null");
		final GefAgeDiagram diagram = checkEditor(editor).getGefDiagram();
		final Bounds bounds = getBounds(getExportNodes(diagram, exportNode),
				diagram.getSceneNode().getSceneToLocalTransform());
		return new Dimension(bounds.getWidth(), bounds.getHeight());
	}

	private GefAgeDiagram loadDiagram(final IFile diagramFile) {
		final URI uri = URI.createPlatformResourceURI(diagramFile.getFullPath().toString(), true);
		final IProject project = ProjectUtil.getProjectOrNull(uri);
		final org.osate.ge.diagram.Diagram mmDiagram = DiagramSerialization.readMetaModelDiagram(uri);
		final IEclipseContext eclipseContext = EclipseContextFactory
				.getServiceContext(FrameworkUtil.getBundle(GefDiagramExportService.class).getBundleContext());
		final ExtensionRegistryService extensionRegistry = Objects.requireNonNull(
				eclipseContext.get(ExtensionRegistryService.class), "Unable to retrieve extension registry");
		final ReferenceService referenceService = Objects.requireNonNull(eclipseContext.get(ReferenceService.class),
				"unable to retrieve reference service");
		final ActionService actionService = Objects.requireNonNull(eclipseContext.get(ActionService.class),
				"unable to retrieve action service");

		final AgeDiagram diagram = DiagramSerialization.createAgeDiagram(project, mmDiagram, extensionRegistry);

		// Update the diagram
		final QueryService queryService = new DefaultQueryService(referenceService);
		final ProjectProvider projectProvider = diagramFile::getProject;
		final ProjectReferenceService projectReferenceService = new ProjectReferenceServiceProxy(referenceService,
				projectProvider);
		final BusinessObjectNodeFactory nodeFactory = new BusinessObjectNodeFactory(
				projectReferenceService);
		final DefaultBusinessObjectTreeUpdater boTreeUpdater = new DefaultBusinessObjectTreeUpdater(projectProvider, extensionRegistry,
				projectReferenceService, queryService, nodeFactory);
		final DefaultDiagramElementGraphicalConfigurationProvider deInfoProvider = new DefaultDiagramElementGraphicalConfigurationProvider(
				queryService, () -> diagram, extensionRegistry);
		final DiagramUpdater diagramUpdater = new DiagramUpdater(boTreeUpdater, deInfoProvider, actionService,
				projectReferenceService, projectReferenceService);
		diagramUpdater.updateDiagram(diagram);

		// Create the GEF Diagram
		final GefAgeDiagram gefDiagram = new GefAgeDiagram(diagram, new DefaultColoringService(
				new org.osate.ge.internal.services.impl.DefaultColoringService.StyleRefresher() {
					@Override
					public void refreshDiagramColoring() {
						// No-op. Handling coloring service refresh requests is not required.
					}

					@Override
					public void refreshColoring(final Collection<DiagramElement> diagramElements) {
						// No-op. Handling coloring service refresh requests is not required.
					}
				}));

		// Add to scene. This is required for text rendering
		new Scene(gefDiagram.getSceneNode());

		// Update the diagram to reflect the scene graph and perform incremental layout
		gefDiagram.updateDiagramFromSceneGraph(false);
		diagram.modify("Incremental Layout", m -> DiagramElementLayoutUtil.layoutIncrementally(diagram, m, gefDiagram));

		return gefDiagram;
	}

	/**
	 * Returns true if the node is an export node. An export node is a node which results in drawing while exporting.
	 * @param node the node to check.
	 * @return true if the node is an export node.
	 */
	private static boolean isExportNode(final Node node) {
		return node instanceof Path || node instanceof CubicCurve || node instanceof Polyline || node instanceof Polygon
				|| node instanceof javafx.scene.shape.Rectangle || node instanceof Circle || node instanceof Ellipse
				|| node instanceof Text || node instanceof ImageView
				|| (node instanceof Region && ((Region) node).getBackground() != null);
	}

	/**
	 * Returns a list of nodes to export. The nodes will be in draw order.
	 * @param diagram the diagram being export
	 * @param exportRootDiagramNode the root node being exported.
	 * @return the list of nodes to export.
	 */
	private static List<Node> getExportNodes(final GefAgeDiagram diagram, final DiagramNode exportRootDiagramNode) {
		// final GefAgeDiagram diagram,
		final Node exportRootSceneNode = diagram.getSceneNode(exportRootDiagramNode);
		if (exportRootSceneNode == null) {
			throw new AgeGefRuntimeException("Unable to find scene node for specified diagram node");
		}

		final List<Node> exportNodes = new ArrayList<>();
		addNonConnectionExportNodes(exportRootSceneNode, exportNodes);
		addConnectionExportNodes(diagram.getSceneNode(), exportRootSceneNode, exportNodes);

		return exportNodes;
	}

	/**
	 * Adds export nodes which are not descendants of a connection.
	 * @param node is the node being traversed.
	 * @param nodes the list to which to add the export nodes
	 */
	private static void addNonConnectionExportNodes(final Node node, List<Node> nodes) {
		// Don't export connections
		if (node.isVisible()) {
			if (node instanceof Connection) {
				return;
			}

			if (isExportNode(node)) {
				nodes.add(node);
			}

			if (node instanceof Parent) {
				for (final Node child : ((Parent) node).getChildrenUnmodifiable()) {
					addNonConnectionExportNodes(child, nodes);
				}
			}
		}
	}

	/**
	 * Adds export nodes which are descendants of a connection.
	 * @param node is the node being traversed.
	 * @param exportRoot is the root node being exported. Connections will not be added unless both ends of the
	 * connection are within this node.
	 * @param nodes the list to which to add the export nodes
	 */
	private static void addConnectionExportNodes(final Node node, final Node exportRoot, List<Node> nodes) {
		// Don't export connections
		if (node.isVisible()) {
			// Add the connection
			if (node instanceof Connection) {
				final Connection cn = (Connection) node;

				// Check if both end points are being exported
				if (!isExported(cn.getStartAnchor(), exportRoot) || !isExported(cn.getEndAnchor(), exportRoot)) {
					return;
				}

				for (final Node child : ((Parent) node).getChildrenUnmodifiable()) {
					// Add non-connection descendants
					addNonConnectionExportNodes(child, nodes);
				}
			} else if (node instanceof Parent) {
				// Continue looking for children
				for (final Node child : ((Parent) node).getChildrenUnmodifiable()) {
					addConnectionExportNodes(child, exportRoot, nodes);
				}
			}
		}
	}

	/**
	 * Returns true if the specified anchor will be exported.
	 * @param anchor the anchor to check
	 * @param exportRoot the root of the export
	 * @return true if the anchor is n the export
	 */
	private static boolean isExported(final IAnchor anchor, final Node exportRoot) {
		if (anchor == null) {
			return false;
		}

		// Check if the anchor is not owned by any node or is owned by a connection. If it is owned by a connection,
		// we assume it will be exported.
		if (anchor.getAnchorage() == null || anchor.getAnchorage() instanceof BaseConnectionNode) {
			return true;
		}

		for (Node tmp = anchor.getAnchorage(); tmp != null; tmp = tmp.getParent()) {
			if (tmp == exportRoot) {
				return true;
			}
		}

		return false;
	}

	private static void draw(final List<Node> exportNodes, final Transform sceneToExportTransform, final double scaling,
			final Graphics2D g2d) {
		// Set rendering options
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
		g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
		g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);

		// Scale all drawing
		g2d.scale(scaling, scaling);

		for (final Node node : exportNodes) {
			// Create a new graphics object which uses the node's coordinate system
			final Graphics2D tg = (Graphics2D) g2d.create();
			final Transform nodeToDiagramTransform = sceneToExportTransform
					.createConcatenation(node.getLocalToSceneTransform());
			tg.transform(new AffineTransform(nodeToDiagramTransform.getMxx(), nodeToDiagramTransform.getMyx(),
					nodeToDiagramTransform.getMxy(), nodeToDiagramTransform.getMyy(), nodeToDiagramTransform.getTx(),
					nodeToDiagramTransform.getTy()));

			// Draw the node
			if (node instanceof Path) {
				final Path n = (Path) node;
				draw(toAwt(n), tg, toAwt(n.getFill()), toAwt(n.getStroke()), (float) n.getStrokeWidth(),
						n.getStrokeDashArray());
			} else if (node instanceof javafx.scene.shape.Rectangle) {
				final javafx.scene.shape.Rectangle n = (javafx.scene.shape.Rectangle) node;
				draw(toAwt(n), tg, toAwt(n.getFill()), toAwt(n.getStroke()), (float) n.getStrokeWidth(),
						n.getStrokeDashArray());
			} else if (node instanceof javafx.scene.shape.Circle) {
				final javafx.scene.shape.Circle n = (javafx.scene.shape.Circle) node;
				draw(toAwt(n), tg, toAwt(n.getFill()), toAwt(n.getStroke()), (float) n.getStrokeWidth(),
						n.getStrokeDashArray());
			} else if (node instanceof javafx.scene.shape.Ellipse) {
				final javafx.scene.shape.Ellipse n = (javafx.scene.shape.Ellipse) node;
				draw(toAwt(n), tg, toAwt(n.getFill()), toAwt(n.getStroke()), (float) n.getStrokeWidth(),
						n.getStrokeDashArray());
			} else if (node instanceof CubicCurve) {
				// Draw unfilled curve
				final CubicCurve n = (CubicCurve) node;
				draw(toAwt(n), tg, null, toAwt(n.getStroke()), (float) n.getStrokeWidth(), n.getStrokeDashArray());
			} else if (node instanceof Polygon) {
				final javafx.scene.shape.Polygon n = (javafx.scene.shape.Polygon) node;
				draw(toAwtPath(n.getPoints(), true), tg, toAwt(n.getFill()), toAwt(n.getStroke()),
						(float) n.getStrokeWidth(), n.getStrokeDashArray());
			} else if (node instanceof Polyline) {
				final javafx.scene.shape.Polyline n = (javafx.scene.shape.Polyline) node;
				// Draw unfilled polyline
				draw(toAwtPath(n.getPoints(), false), tg, null, toAwt(n.getStroke()), (float) n.getStrokeWidth(),
						n.getStrokeDashArray());
			} else if (node instanceof Text) {
				draw((Text) node, tg);
			} else if (node instanceof ImageView) {
				draw((ImageView) node, tg);
			} else if (node instanceof Region) {
				draw((Region) node, tg);
			} else {
				throw new AgeGefRuntimeException("Unexpected export node: " + node);
			}
		}
	}

	private static void draw(final Text text, final Graphics2D g2d) {
		// Draw the text using a JTextArea so that line wrapping will be supported.
		final Bounds layoutBounds = text.getLayoutBounds();
		final JTextArea ta = new JTextArea(text.getText());
		ta.setLineWrap(text.getWrappingWidth() > 0);
		ta.setWrapStyleWord(true);
		ta.setBackground(TRANSPARENT);
		ta.setBounds(0, 0, (int) (text.getWrappingWidth() > 0 ? text.getWrappingWidth() : layoutBounds.getWidth()),
				(int) layoutBounds.getHeight());
		ta.setForeground(toAwt(text.getFill()));
		ta.setFont(toAwt(text.getFont()));
		g2d.translate((int) layoutBounds.getMinX(), (int) layoutBounds.getMinY());
		ta.paint(g2d);
	}

	private static void draw(final ImageView imageView, final Graphics2D g2d) {
		final Bounds layoutBounds = imageView.getLayoutBounds();
		final BufferedImage image = SwingFXUtils.fromFXImage(imageView.getImage(), null);
		g2d.drawImage(image, (int) layoutBounds.getMinX(), (int) layoutBounds.getMinY(), (int) layoutBounds.getMaxX(),
				(int) layoutBounds.getMaxY(), 0, 0, image.getWidth(), image.getHeight(), null, null);
	}

	private static void draw(final Region region, final Graphics2D g2d) {
		// For regions, only drawing background is supported
		if (region.getBackground() != null) {
			final Bounds layoutBounds = region.getLayoutBounds();
			for (final BackgroundFill f : region.getBackground().getFills()) {
				final Insets insets = f.getInsets();
				final Shape shape = new Rectangle.Double(layoutBounds.getMinX() + insets.getLeft(),
						layoutBounds.getMinY() + insets.getTop(),
						layoutBounds.getWidth() - insets.getLeft() - insets.getRight(),
						layoutBounds.getHeight() - insets.getTop() - insets.getBottom());
				draw(shape, g2d, toAwt(f.getFill()), null, 1.0f, Collections.emptyList());
			}
		}
	}

	/**
	 * Draws a shape.
	 * This method attempts to draw shapes consistent with the usage of JavaFX in the graphical editor. However, when
	 * rendering non-SVG graphics, the stroke will be drawn along the shape's outline rather than inside of it. This is due
	 * to quality issues when attempting to draw the stroke inside by clipping along the outline.
	 * @param shape the shape to draw
	 * @param g2d the {@link Graphics2D} instance to use to draw the shape
	 * @param fill the paint for filling the shape. If null, the shape will not be filled.
	 * @param stroke the paint for stroking the shape. If null, the shape will not be stroked.
	 * @param strokeWidth the stroke width
	 * @param strokeDashArray the dash array that determines dash sizes. An empty list indicates a solid stroke.
	 */
	private static void draw(final Shape shape, final Graphics2D g2d, final java.awt.Paint fill,
			final java.awt.Paint stroke, final float strokeWidth, final List<Double> strokeDashArray) {

		// Clipping is used to draw the stroke along the inside. This only works properly for SVG graphics.
		// We assume non-filled shapes are likely connections or similar graphics and that they should not be clipped.
		// Clipping such graphics may result in them not visible.
		final boolean clip = g2d instanceof SVGGraphics2D && fill != null;
		if (clip) {
			g2d.setClip(shape);
		}

		if (fill != null) {
			g2d.setPaint(fill);
			g2d.fill(shape);
		}

		if (stroke != null) {
			// Convert stroke dash array list to a float array
			final float[] dashArray;
			if (strokeDashArray.isEmpty()) {
				dashArray = null;
			} else {
				dashArray = new float[strokeDashArray.size()];
				int i = 0;
				for (final Double value : strokeDashArray) {
					dashArray[i++] = value.floatValue();
				}
			}

			g2d.setStroke(new BasicStroke(g2d.getClip() == null ? strokeWidth : strokeWidth * 2.0f,
					BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dashArray, 0.0f));
			g2d.setPaint(stroke);
			g2d.draw(shape);
		}

		if (clip) {
			g2d.setClip(null);
		}
	}

	private static Path2D.Double toAwt(final Path p) {
		final Path2D.Double result = new Path2D.Double(
				p.getFillRule() == FillRule.EVEN_ODD ? Path2D.WIND_EVEN_ODD : Path2D.WIND_NON_ZERO,
				p.getElements().size());

		double lastX = 0;
		double lastY = 0;
		for (final PathElement e : p.getElements()) {
			if (e instanceof ClosePath) {
				result.closePath();
			} else if (e instanceof HLineTo) {
				final HLineTo el = (HLineTo) e;
				lastX = el.getX();
				result.lineTo(lastX, lastY);
			} else if (e instanceof VLineTo) {
				final VLineTo el = (VLineTo) e;
				lastY = el.getY();
				result.lineTo(lastX, lastY);
			} else if (e instanceof LineTo) {
				final LineTo el = (LineTo) e;
				lastX = el.getX();
				lastY = el.getY();
				result.lineTo(lastX, lastY);
			} else if (e instanceof MoveTo) {
				final MoveTo el = (MoveTo) e;
				lastX = el.getX();
				lastY = el.getY();
				result.moveTo(lastX, lastY);
			} else if (e instanceof CubicCurveTo) {
				final CubicCurveTo el = (CubicCurveTo) e;
				lastX = el.getX();
				lastY = el.getY();
				result.curveTo(el.getControlX1(), el.getControlY1(), el.getControlX2(), el.getControlY2(), lastX,
						lastY);
			} else if (e instanceof QuadCurveTo) {
				final QuadCurveTo el = (QuadCurveTo) e;
				lastX = el.getX();
				lastY = el.getY();
				result.quadTo(el.getControlX(), el.getControlY(), lastX, lastY);
			} else if (e instanceof ArcTo) {
				// Path2D does not have an equivalent to ArcTo. However, it is not used the graphical editor.
				// If ArcTo is used it will need to be implemented.
				throw new AgeGefRuntimeException("Exporting ArcTo path segments is not supported.");
			} else {
				throw new AgeGefRuntimeException("Unexpected path element: " + e);
			}
		}

		return result;
	}

	private static CubicCurve2D.Double toAwt(final CubicCurve c) {
		return new CubicCurve2D.Double(c.getStartX(), c.getStartY(), c.getControlX1(), c.getControlY1(),
				c.getControlX2(), c.getControlY2(), c.getEndX(), c.getEndY());
	}

	private static Path2D.Double toAwtPath(final List<Double> points, final boolean polygon) {
		final Path2D.Double result = new Path2D.Double(Path2D.WIND_NON_ZERO, points.size());
		if (points.size() >= 4) {
			result.moveTo(points.get(0), points.get(1));
			for (int i = 2; i < (points.size() - 1); i += 2) {
				result.lineTo(points.get(i), points.get(i + 1));
			}

			if (polygon) {
				result.closePath();
			}
		}
		return result;
	}

	private static Shape toAwt(final javafx.scene.shape.Rectangle r) {
		final Bounds b = r.getLayoutBounds();
		return new RoundRectangle2D.Double(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight(), r.getArcWidth(),
				r.getArcHeight());
	}

	private static Shape toAwt(final Circle c) {
		final Bounds b = c.getLayoutBounds();
		return new Ellipse2D.Double(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight());
	}

	private static Shape toAwt(final javafx.scene.shape.Ellipse e) {
		final Bounds b = e.getLayoutBounds();
		return new Ellipse2D.Double(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight());
	}

	private static java.awt.Color toAwt(final javafx.scene.paint.Paint p) {
		if (p == null) {
			return null;
		} else if (p instanceof javafx.scene.paint.Color) {
			return toAwt((javafx.scene.paint.Color) p);
		} else {
			throw new AgeGefRuntimeException("Unsupported paint: " + p);
		}

	}

	private static java.awt.Color toAwt(final javafx.scene.paint.Color c) {
		if (c == null) {
			return null;
		} else {
			if (c.getOpacity() == 0.0) {
				return null;
			}

			return new java.awt.Color((float) c.getRed(), (float) c.getGreen(), (float) c.getBlue(),
					(float) c.getOpacity());
		}
	}

	private static java.awt.Font toAwt(final javafx.scene.text.Font font) {
		int style = 0;
		if (font.getStyle().contains("Bold")) {
			style |= Font.BOLD;
		}

		if (font.getStyle().contains("Italic")) {
			style |= Font.ITALIC;
		}

		return new java.awt.Font(font.getName(), style, (int) font.getSize()).deriveFont((float) font.getSize());
	}

	/**
	 * Checks the editor. Throws an exception if the editor isn't of type {@link AgeEditor}. If it is of that type, casts and then returns it.
	 * @param editor the editor to check
	 * @return the editor
	 */
	private AgeEditor checkEditor(final GraphicalEditor editor) {
		if (!(editor instanceof AgeEditor)) {
			throw new AgeGefRuntimeException("Unexpected editor type. Editor must be of type " + AgeEditor.class);
		}

		return (AgeEditor) editor;
	}
}