/*
* @(#)JFXImageContext.java
*
* $Date: 2014-11-27 07:50:51 +0100 (Cs, 27 nov. 2014) $
*
* Copyright (c) 2014 by Jeremy Wood.
* All rights reserved.
*
* The copyright of this software is owned by Jeremy Wood.
* You may not use, copy or modify this software, except in
* accordance with the license agreement you entered into with
* Jeremy Wood. For details see accompanying license terms.
*
* This software is probably, but not necessarily, discussed here:
* https://javagraphics.java.net/
*
* That site should also contain the most recent official version
* of this software. (See the SVN repository for more details.)
*/
package com.bric.image;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/** This is an <code>ImageContext</code> that uses JavaFX.
*/
public class JFXImageContext extends ImageContext {
private boolean realized = false;
private BufferedImage buf;
private Canvas canvas;
private GraphicsContext context;
private WritableImage currentImage;
private WritableImage tmpOut;
private SnapshotParameters snapshotParameters = new SnapshotParameters();
private boolean autoFlush = true;
private static boolean isInitJavaFX;
public JFXImageContext(BufferedImage bi) {
initJavaFX();
snapshotParameters.setFill(new Color(1, 1, 1, 0));
buf = bi;
canvas = new Canvas(bi.getWidth(), bi.getHeight());
context = canvas.getGraphicsContext2D();
realized = true;
}
private static void initJavaFX() {
if (!isInitJavaFX) {
isInitJavaFX = true;
new JFXPanel(); // initializes JavaFX environment
}
}
private void ensureRealized() {
if (!realized) {
throw new IllegalStateException("This context has been disposed.");
}
}
/** Changes auto flush behavior.
* <p>
* If you set auto flush to false, you need to invoke {@link #flush} in
* order to copy the image data into the buffered image.
* <p>
* Setting auto flush to false greatly improves performance, because
* the image data does not need be converted from JavaFX to AWT
* after each drawing call.
*
* <p>Note: autoflush is active by default. When I turn it off and render
* two unique images, then only one of those images is eventually rendered
* to the target BufferedImage.
*
* @param b the new auto flush state.
*/
public void setAutoFlush(boolean b) {
this.autoFlush = b;
}
public boolean isAutoFlush() {
return autoFlush;
}
/** Draw an image to this Graphics3D.
* <p>This respects the interpolation rendering hints. When the
* interpolation hint is missing, this will also consult the antialiasing
* hint or the render hint. The bilinear hint is used by default.
* <p>This uses a source over composite.
* <p>This sets the current image.
*
* <p>Note: if you want to draw the same image multiple times, it is faster
* to use the following approach:
* <pre>
* GraphicsFX g = ...;
* g.setAutoFlush(false);
* g.setImage(img);
* for (...) {
* g.drawImage(topLeft,topRight,bottomRight,bottomLeft);
* }
* g.flush();
* </pre>
* instead.
*
*
* @param img the image to draw.
* @param topLeft where the top-left corner of this image will be painted.
* @param topRight where the top-right corner of this image will be painted.
* @param bottomRight where the bottom-right corner of this image will be painted.
* @param bottomLeft where the bottom-left corner of this image will be painted.
*/
public void drawImage(BufferedImage img, Point2D topLeft, Point2D topRight, Point2D bottomRight, Point2D bottomLeft) {
setImage(img);
drawImage(topLeft, topRight, bottomRight, bottomLeft);
}
/** Draw an image to this Graphics3D.
* <p>This respects the interpolation rendering hints. When the
* interpolation hint is missing, this will also consult the antialiasing
* hint or the render hint. The bilinear hint is used by default.
* <p>This uses a source over composite.
* <p>This sets the current image.
*
* <p>Note: if you want to draw the same image multiple times, it is faster
* to use the following approach:
* <pre>
* GraphicsFX g = ...;
* g.setAutoFlush(false);
* g.setImage(img);
* for (...) {
* g.drawImage(topLeft,topRight,bottomRight,bottomLeft);
* }
* g.flush();
* </pre>
* instead.
*
*
* @param img the image to draw.
* @param topLeft where the top-left corner of this image will be painted.
* @param topRight where the top-right corner of this image will be painted.
* @param bottomRight where the bottom-right corner of this image will be painted.
* @param bottomLeft where the bottom-left corner of this image will be painted.
*/
public void drawImage(WritableImage img, Point2D topLeft, Point2D topRight, Point2D bottomRight, Point2D bottomLeft) {
setImage(img);
drawImage(topLeft, topRight, bottomRight, bottomLeft);
}
/** Sets the current image for consecutively drawing the same image.
*
* @param img the current image to paint.
*/
public void setImage(BufferedImage img) {
currentImage = toImage(img, currentImage);
}
/** Sets the current image for consecutively drawing the same image.
*
* @param img the current image to paint.
*/
public void setImage(WritableImage img) {
currentImage = img;
}
/** Converts the provided BufferedImage into a JavaFX Image.
* @param img The BufferedImage
* @param reuseMe Attempts to reuse this image. Specify null to force creation
* of a new image.
* @return The converted image. May be the same instance as {@code reuseMe}.
*/
public WritableImage toImage(BufferedImage img, WritableImage reuseMe) {
return SwingFXUtils.toFXImage(img, reuseMe);
}
/** Draws the current image.
* @param topLeft where the top-left corner of the current image should be rendered
* @param topRight where the top-right corner of the current image should be rendered
* @param bottomRight where the bottom-right corner of the current image should be rendered
* @param bottomLeft where the bottom-left corner of the current image should be rendered
*/
public void drawImage(Point2D topLeft, Point2D topRight, Point2D bottomRight, Point2D bottomLeft) {
ensureRealized();
PerspectiveTransform t = new PerspectiveTransform();
t.setUlx(topLeft.getX());
t.setUly(topLeft.getY());
t.setUrx(topRight.getX());
t.setUry(topRight.getY());
t.setLrx(bottomRight.getX());
t.setLry(bottomRight.getY());
t.setLlx(bottomLeft.getX());
t.setLly(bottomLeft.getY());
context.setEffect(t);
context.drawImage(currentImage, 0, 0);
if (autoFlush) {
flush();
}
}
/** Renders the current graphics state into the BufferedImage. */
public void flush() {
FutureTask<Void> r = new FutureTask<>(new Runnable() {
@Override
public void run() {
tmpOut = canvas.snapshot(snapshotParameters, tmpOut);
BufferedImage newBuf = SwingFXUtils.fromFXImage(tmpOut, buf);
if (newBuf != buf) {
//Graphics2D g = buf.createGraphics();
//g.drawImage(newBuf, 0, 0, null);
//g.dispose();
// System.err.println(newBuf.getWidth()+"x"+newBuf.getHeight()+", "+Reflection.nameStaticField(BufferedImage.class, newBuf.getType()));
// System.err.println(buf.getWidth()+"x"+buf.getHeight()+", "+Reflection.nameStaticField(BufferedImage.class, buf.getType()));
throw new InternalError("Allocated a new buffer instead of reusing the existing one.");
}
}
}, null);
Platform.runLater(r);
try {
r.get();
} catch (InterruptedException | ExecutionException ex) {
InternalError e = new InternalError();
e.initCause(ex);
throw e;
}
}
/** Commit all changes back to the BufferedImage this context paints to.
*/
public void dispose() {
if (realized) {
flush();
realized = false;
canvas = null;
context = null;
buf = null;
currentImage = null;
}
}
public void setRenderingHint(RenderingHints.Key KEY_INTERPOLATION, Object VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
/// ignore this for now
}
}