Render GUI to Image in Memory - java

Is it somehow possible to render a GUI to a BufferedImage or another kind of memory image without displaying it on a screen ?
I know this will loose all kinds of hardware acceleration, but for a simple GUI that is refreshed only once or twice a second this should not be an issue.
Already tried to get JavaFX to output an image, but i can't find a way to leave out rendering on a screen first. Does anyone know a way to do this with JavaFX or Swing ?
It is no problem to draw a simple GUI myself using simple image manipulations, but then i would have to do it all by hand and using Swing or FX would make it much easier.
Edit:
To make it a bit more clear, i don't have an active display, but i can save an image which then gets displayed through other means. To be exact its a raspberry pi, but without a primary display device with a connected tft display using the GPIO port. So i can't render the UI directly to a display device, but need to create an image that i can save at a specific location. All methods i have tried so far need a primary display device.

Yes, it is possible to render a GUI to an image offscreen.
Here is a sample using JavaFX, with example image output as below:
The example works by rendering the chart to an scene which is not added to any window and no window (Stage in JavaFX terminology) is ever shown. The snapshot method is used to take a snapshot of the node and then ImageIO utilities are used to save the snapshot to disk.
Rendering of the offscreen scene will be hardware accelerated if the underlying hardware/software platform supports it.
import javafx.application.*;
import javafx.collections.*;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.*;
import javafx.scene.chart.PieChart;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.*;
public class OffscreenImageRecorder extends Application {
private static final Logger logger = Logger.getLogger(OffscreenImageRecorder.class.getName());
private static final String IMAGE_TYPE = "png";
private static final String IMAGE_FILENAME = "image." + IMAGE_TYPE;
private static final String WORKING_DIR = System.getProperty("user.dir");
private static final String IMAGE_PATH = new File(WORKING_DIR, IMAGE_FILENAME).getPath();
private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
private final Random random = new Random();
private final int CHART_SIZE = 400;
#Override
public void start(Stage stage) throws IOException {
Parent chart = createChart();
Image image = snapshot(chart);
exportPng(SwingFXUtils.fromFXImage(image, null), IMAGE_PATH);
Platform.exit();
}
private Parent createChart() {
// create a chart.
final PieChart chart = new PieChart();
ObservableList<PieChart.Data> pieChartData =
FXCollections.observableArrayList(
new PieChart.Data("Grapefruit", random.nextInt(30)),
new PieChart.Data("Oranges", random.nextInt(30)),
new PieChart.Data("Plums", random.nextInt(30)),
new PieChart.Data("Pears", random.nextInt(30)),
new PieChart.Data("Apples", random.nextInt(30))
);
chart.setData(pieChartData);
chart.setTitle("Imported Fruits - " + dateFormat.format(new Date()));
// It is important for snapshots that the chart is not animated
// otherwise we could get a snapshot of the chart before the
// data display has been animated in.
chart.setAnimated(false);
chart.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
chart.setPrefSize(CHART_SIZE, CHART_SIZE);
chart.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
return chart;
}
private Image snapshot(final Parent sourceNode) {
// Note: if the source node is not in a scene, css styles will not
// be applied during a snapshot which may result in incorrect rendering.
final Scene snapshotScene = new Scene(sourceNode);
return sourceNode.snapshot(
new SnapshotParameters(),
null
);
}
private void exportPng(BufferedImage image, String filename) {
try {
ImageIO.write(image, IMAGE_TYPE, new File(filename));
logger.log(Level.INFO, "Wrote image to: " + filename);
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
public static void main(String[] args) {
launch(args);
}
}

It's a bit of a hack, but you could create a frame and position it on a invisible location (using Swing in this example):
frame = new JFrame("Invisible frame");
frame.setBounds(-1000, 100, 640, 480);

Related

Select with mouse on a PDF (User Input)

I'm currently using PDFBox and I'm trying to open a PDF so that the user can select with his mouse areas to crop, I've no idea how to proceed to make a PDF viewer & let the user input the selected rectangle to crop.
PDPage#setCropBox(PDRectangle cropBox)
https://pdfbox.apache.org/docs/2.0.2/javadocs/org/apache/pdfbox/pdmodel/PDPage.html#setCropBox(org.apache.pdfbox.pdmodel.common.PDRectangle)
https://pdfbox.apache.org/docs/2.0.2/javadocs/org/apache/pdfbox/pdmodel/common/PDRectangle.html
The only thing I'm missing here is how the user can decide to which point he wants to crop (and I'm thinking of a mouse selection & so having the view of the PDF to select which area to crop), otherwise if he inputs random values, it won't be accurate.
package uk.mushow.pdftoexcel;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import java.io.File;
import java.io.IOException;
public class PDFToExcel extends Application {
public static void main(String[] args) {
launch();
}
#Override
public void start(final Stage stage) {
stage.setTitle("PDF Test");
final FileChooser fileChooser = new FileChooser();
final Button openButton = new Button("Select pdf");
openButton.setOnAction(event -> {
configureFileChooser(fileChooser);
File file = fileChooser.showOpenDialog(stage);
if (file != null) {
try {
openFile(file);
} catch (IOException e) {
e.printStackTrace();
}
}
});
final GridPane inputGridPane = new GridPane();
GridPane.setConstraints(openButton, 0, 1);
inputGridPane.setHgap(6);
inputGridPane.setVgap(6);
inputGridPane.getChildren().add(openButton);
final Pane rootGroup = new VBox(60);
rootGroup.getChildren().add(inputGridPane);
rootGroup.setPadding(new Insets(60, 60, 60, 60));
stage.setScene(new Scene(rootGroup));
stage.show();
}
private void configureFileChooser(final FileChooser fileChooser) {
fileChooser.setTitle("pdf selector");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("PDF", "*.pdf"));
}
private void openFile(File file) throws IOException {
PDDocument pdDocument = PDDocument.load(file);
//This is where I don't know how to handle the file
PDPage pdPage = pdDocument.getPage(0);
//Need the user to select his own cropbox but can't be just values from his head otherwise it wouldn't be accurate);
}
}
Thanks!
EDIT: ADDED IMAGE
How would a user know the coordinates to select that green rectangle?
The best method for getting co-ordinates in most pdf view/annotators is by adding a highlight. and that usually provides some user feed back so the top left of this rescaled example is 45 mm from the left and 111.7mm down from the top (however that alone is not the full picture since the real units have been converted to mm so you need to export the highlight data into a different set of units, which a different viewer may use. Both are corrrect but just showing the values via different outputs.
You need to "Square" up those differences and an export of comments usually does that, however the maths can be a challenge as its done in PDF page co-ordinates.
"rect": [
227.495148,
351.695496,
386.449585,
430.445496
],
"subtype": "Popup",
"annotationType": 16,
"parentType": "Square",
"parentId": "6R",
"parentRect": [
128.322571,
24.175232,
210.179276,
485.779327
],
You will need to experiment with your chosen libraries as to which is easiest, personally for my uses, I would look towards milking command line utilities to do the maths and text output for me.

How to add a timestamp in java on a pane

I am currently working on this assignment and I can not seem to get this program to run even though I don't have any errors really popping up ? I am trying to add a time stamp to the pane as well but every time I add the "ts" name for the time stamp to the Pane or Hbox's get children code it goes red.. I am not sure what exactly I'm doing wrong if anyone can point me in the right direction id greatly appreciate it...
package PCK1;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import java.sql.Timestamp;
import java.util.Date;
import java.text.SimpleDateFormat;
public class MainClass
{
public static void start(Stage stage)
{
// Time Stamp
Date date = new Date();
Timestamp ts=new Timestamp(date.getTime());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(formatter.format(ts));
//Create a Circle
Circle c1 = new Circle(75,100,20);
//Create a Pane
Pane p = new Pane();
p.setMinSize(100, 150);
p.setBackground(new Background(new BackgroundFill( Color.rgb(190, 220, 190), null, null)
));
p.getChildren().addAll(c1);
//Create a Button
Button btnUp = new Button("Up");
btnUp.setOnAction((ActionEvent e) -> {double y = c1.getCenterY();
y -= 20.0;
c1.setCenterY(y);
});
Button btnDown = new Button("Down");
btnDown.setOnAction((ActionEvent e) -> {double y = c1.getCenterY();
y += 20.0;
c1.setCenterY(y);
});
//Create a HBox
HBox hb = new HBox();
hb.getChildren().addAll(btnUp, btnDown, p, ts);
hb.setBackground(new Background(new BackgroundFill(Color.rgb(150,200,150),null,null)));
hb.setMinSize(100, 50);
hb.setPadding(new Insets(10,10,10,10));
Scene scene = new Scene(hb);
stage.setScene(scene);
stage.setTitle("JavaFx");
stage.setWidth(250);
stage.setHeight(250);
stage.show();
}
}
Answer for displaying a timestamp
Specifically, for the timestamp question, see the following example code:
private Label createTimestampLabel() {
LocalDateTime now = LocalDateTime.now();
String formattedTimestamp = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return new Label(formattedTimestamp);
}
It uses the java.time APIs explained in the Oracle Date Time tutorial to get the current time from LocalDateTime and format it as a String using a standard format.
It sets the formatted timestamp string as the text of a Label node.
Now that the returned element is a Node, it can be placed in the scene graph without generating the compile error you saw in your original example.
Using the java.time APIs is preferred over the java.sql.Timestamp and java.util.Date code in your question. You are not working with SQL, so you should not be using java.sql.Timestamp. The java.time classes also have many improvements over obsolete date and time functions used in other Java packages like java.util.
Answer in context with a re-write of your example code
There were a lot of things about the provided example application that were either wrong or annoyed me.
So I re-wrote it to match a bit more closely how I would normally write such an application.
There are maybe a hundred different small decisions made in the choices for how to implement the re-write and explaining them all here would be too verbose.
Hopefully, you can compare the re-write to your original code, note some of the differences, and learn some things from it.
GraphicControlApp.java
package org.example.javafx.demo.graphiccontrol;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class GraphicControlApp extends Application {
public void start(Stage stage) {
GraphicController graphicController = new GraphicController();
Scene scene = new Scene(graphicController.getUI());
stage.setScene(scene);
stage.setTitle("JavaFX Interactive Graphic Control Demonstration");
stage.show();
}
}
GraphicController.java
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* UI creator and controller for application logic.
*
* Normally, most UI elements would be defined externally in FXML,
* however, for a simple application, we define the UI via private functions in this class.
*/
public class GraphicController {
// amount to move the circle across the surface on interaction.
private static final double MOVEMENT_DELTA = 20.0;
// default spacing between UI elements.
private static final double SPACING = 10;
// normally the styles would be configured in an external css stylesheet,
// but we place the background definitions here for a simple application.
private static final Color SURFACE_COLOR = Color.rgb(190, 220, 190);
private static final Background surfaceBackground = createBackground(SURFACE_COLOR);
private static final Color APP_BACKGROUND_COLOR = Color.rgb(150, 200, 150);
private static final Background appBackground = createBackground(APP_BACKGROUND_COLOR);
private Button up;
private Button down;
/**
* #return the complete layout for the application with event handlers attached for logic control.
*/
public Pane getUI() {
Circle circle = new Circle(75, 100, 20);
Pane surface = createSurface(circle);
HBox controls = createControls(circle);
Label timestampLabel = createTimestampLabel();
Pane layout = createLayout(surface, controls, timestampLabel);
attachKeyboardHandlers(layout);
return layout;
}
/**
* Create a label formatted with the current time in ISO standard format (e.g. '2011-12-03T10:15:30')
*
* #return label with the current timestamp.
*/
private Label createTimestampLabel() {
LocalDateTime now = LocalDateTime.now();
String formattedTimestamp = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return new Label(formattedTimestamp);
}
/**
* Create a surface on which a circle can move.
*
* #param circle the circle which can move on the surface.
* #return the created surface.
*/
private Pane createSurface(Circle circle) {
Pane surface = new Pane();
surface.setMinSize(100, 150);
surface.setBackground(surfaceBackground);
surface.getChildren().addAll(circle);
// we must define a clip on the surface to ensure that elements
// in the surface do not render outside the surface.
Rectangle clip = new Rectangle();
clip.widthProperty().bind(surface.widthProperty());
clip.heightProperty().bind(surface.heightProperty());
surface.setClip(clip);
return surface;
}
private VBox createLayout(Pane surface, HBox controls, Label timestampLabel) {
VBox layout = new VBox(SPACING, controls, surface, timestampLabel);
layout.setBackground(appBackground);
layout.setPadding(new Insets(SPACING));
VBox.setVgrow(surface, Priority.ALWAYS);
return layout;
}
/**
* Create controls which can control the movement of a circle.
*
* #param circle the circle which can be controlled
* #return the created controls with handlers attached for circle movement control.
*/
private HBox createControls(Circle circle) {
up = new Button("Up");
up.setOnAction(e -> moveVertically(circle, -MOVEMENT_DELTA));
down = new Button("Down");
down.setOnAction(e -> moveVertically(circle, MOVEMENT_DELTA));
return new HBox(SPACING, up, down);
}
private void moveVertically(Circle circle, double delta) {
double y = circle.getCenterY();
// we only restrict movement in the up direction,
// but allow unlimited movement in the down direction
// (even if that movement would mean that the circle would extend totally
// outside the current visible boundary of the surface).
if ((y + delta) < 0) {
return;
}
circle.setCenterY(y + delta);
}
/**
* Adds standard keyboard handling logic to the UI.
*
* Handlers are attached to the relevant scene whenever
* the scene containing the UI changes.
*
* #param layout the UI which will respond to keyboard input.
*/
private void attachKeyboardHandlers(Pane layout) {
EventHandler<KeyEvent> keyEventHandler = event -> {
switch (event.getCode()) {
case UP -> { up.requestFocus(); up.fire(); }
case DOWN -> { down.requestFocus(); down.fire(); }
}
};
layout.sceneProperty().addListener((observable, oldScene, newScene) -> {
if (oldScene != null) {
oldScene.removeEventFilter(
KeyEvent.KEY_PRESSED,
keyEventHandler
);
}
if (newScene != null) {
newScene.addEventFilter(
KeyEvent.KEY_PRESSED,
keyEventHandler
);
}
});
}
private static Background createBackground(Color surfaceColor) {
return new Background(new BackgroundFill(surfaceColor, null, null));
}
}
You should show the timestamp as text with the TextField (Doc) :
TextField myText = new TextField();
myText.setText("Time: " + formatter.format(ts));
// set what you want to the TextField object: padding, size, color etc...
p.getChildren().addAll(myText);

JavaFX: interaction between AnimationTimer and MenuBar

I am developing a software which gets an image from a camera and displays it live in a JavaFX ImageView. I have a thread which gets the last image (in this case a BufferedImage), and an AnimationTimer that assigns it to the ImageView. The reason I went ahead with an AnimationTimer was it seemed better than to fill the Platform with Runnable each time a new image is obtained. The refresh works fine, and FPS are decent.
However, I noticed that when the AnimationTimer was running, the menu bar in my software was not displayed properly. Some menu items went missing when I moused over others. This picture explains it:
On the left side, you have what the menu normally looks like, and on the right side, what it looks like when the AnimationTimer is running. As you can see, the "Save" menu item is missing, and the background of my live image is displayed instead. Moreover, when I was opening a new window (on a new Scene), when I moused over any kind of Node (a button, a checkbox...), the background turned black. I was able to fix this problem by setting the depth-buffer boolean to true when initializing the Scene. I have however no clue how to fix this menu bar bug, and I think these bugs show what I am doing is probably not right.
I was thinking maybe the JavaFX application thread was saturated with new images to display, and it was basically taking too much time for other elements (such as a menu item) to be painted.
Questions:
Is that really where this bug comes from?
Is there a way to improve my code, using something different from an AnimationTimer for instance?
Here's a code snippet that reproduces the bug. Change the two strings in the start function to path to images. The images should be relatively large (several MB).
Click on the "Start" button to start the animation timer. Then try to open the "File" menu, and mouse over the menu items. The bug does not appear systematically, try repeating moving your mouse up and down over, it should appear at some point.
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
public class ImageRefresher extends Application {
#Override
public void start(Stage primaryStage) {
//Here change the 2 Strings to a path to an image on your HDD
//The bug appears more easily with large images (>3-4MB)
String pathToImage1 = "/path/to/your/first/image";
String pathToImage2 = "/path/to/your/second/image";
try {
//Image content (contains buffered image, see below)
ImageContent image = new ImageContent(pathToImage1);
//If this line is commented, the bug does not appear
image.setImage(ImageIO.read(new File(pathToImage2)));
//JavaFX class containing nodes (see below)
MainWindow window = new MainWindow(image);
Scene scene = new Scene(window.getPane(), 300, 250);
primaryStage.setTitle("Menu refresh");
primaryStage.setScene(scene);
primaryStage.show();
} catch (IOException ex) {
Logger.getLogger(ImageRefresher.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static void main(String[] args) {
launch(args);
}
public class MainWindow {
private BorderPane pane;
private MenuBar menuBar;
private ImageView displayImage;
private Button startRefreshingButton;
private ImageContent imageContent;
private AnimationTimer animationTimer;
public MainWindow(ImageContent imageContent) {
this.imageContent = imageContent;
//Builds the window's components
buildGraphic();
//The image is reset at each frame
animationTimer = new AnimationTimer() {
#Override
public void handle(long now) {
displayImage.setImage(imageContent.getDisplayableImage());
}
};
}
private void buildGraphic() {
pane = new BorderPane();
menuBar = new MenuBar();
Menu menu = new Menu("File");
menu.getItems().addAll(new MenuItem("Save"),
new MenuItem("Open"),
new MenuItem("Close"));
menuBar.getMenus().add(menu);
displayImage = new ImageView();
startRefreshingButton = new Button("Start");
startRefreshingButton.setOnAction((event) -> {
animationTimer.start();
});
pane.setTop(menuBar);
pane.setCenter(displayImage);
pane.setBottom(startRefreshingButton);
}
public Pane getPane() {
return pane;
}
}
public class ImageContent {
private BufferedImage imageContent;
//Initializes bufferedimage with the path specified
public ImageContent(String pathToImage) throws IOException {
imageContent = ImageIO.read(new File(pathToImage));
}
public void setImage(BufferedImage newImage) {
imageContent = newImage;
}
//Function called by the animation timer to
//get a JavaFX image from a bufferedimage
public Image getDisplayableImage() {
return SwingFXUtils.toFXImage(imageContent, null);
}
}
}
I guess the issue is that since you're repainting the image every frame, you're overlaying the menu popup with the image. That seems like a bug, but you're also requesting way more work from the FX Application Thread than you need.
Ideally, you should find a way to check if there's really a new image, and only update the image if there's genuinely a new file. (Consider using java.nio.file.Path to represent the file and calling Files.getLastModifiedTime(path).)
For another way to avoid flooding the FX Application Thread with too many Platform.runLater(...) calls, see Throttling javafx gui updates
In the end I didn't file any issue on jira since I was able to solve my problem. The issue came from the way I called SwingFXUtils.toFXImage(imageContent, null). I returned this function's result on every frame, and I am not sure about the details but this probably created a new object every time. A simple way to avoid that is passing a WritableImage as parameter, and binding the ImageProperty value of the ImageView to it.
If I take the MCVE posted above this could something like this (not tested, probably cleaner solutions existing):
public class MainWindow {
private BorderPane pane;
private MenuBar menuBar;
private ImageView displayImage;
private Button startRefreshingButton;
private ImageContent imageContent;
private AnimationTimer animationTimer;
// Here's the value to bind
private ObservableValue<WritableImage> imageProperty;
public MainWindow(ImageContent imageContent) {
//initialization stuff
this.imageProperty = new ObservableValue<>(imageContent.getWritableImage());
displayImage.imageProperty().bind(imageProperty);
//The image is reset at each frame
animationTimer = new AnimationTimer() {
#Override
public void handle(long now) {
SwingFXUtils.toFXImage(imageContent.getBufferedImage(), image.getWritableImage());
}
};
}
}
public class ImageContent {
private BufferedImage imageContent;
private WritableImage writableImage;
//Initializes bufferedimage with the path specified
public ImageContent(String pathToImage) throws IOException {
imageContent = ImageIO.read(new File(pathToImage));
//Get the width and height values from your image
int width = imageContent.getWidth();
int height = imageContent.getHeight();
writableImage = new WritableImage(width, height);
}
public void setImage(BufferedImage newImage) {
imageContent = newImage;
}
public WritableImage getWritableImage() {
return writableImage;
}
public BufferedImage getBufferedImage() {
return imageContent;
}
}
However, this seems to be quite memory intensive now, I'll look into it.

Skew or Distort Image object in Java

Is it possible to skew or distort an Image object in Java? I 'pull' one side of an image out, making it seem closer to me. (LIke 3D).
Any suggestions?
Yes. Lots of ways but I would start with the Advanced Imaging API. It provides a ton of advanced imaging functionality.
But just to do the type of transform that you're talking about you might just need an Affine Transform. Sample results here for the previous link.
You can also do this with JavaFX.
The following example uses PerspectiveTransform and a bit of rotation on the BufferedImage.
It turns this image
into this
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.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
/**
* Distorts images using transformations.
* <p>
* Created by Matthias Braun on 2018-09-05.
*/
public class Distortion {
public static void main(String... args) throws IOException {
URL imgUrl = new URL("https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png?v=9c558ec15d8a");
BufferedImage img = ImageIO.read(imgUrl);
BufferedImage distorted = distortImg(img);
File newImgFile = new File(System.getenv("HOME") + "/distorted.png");
System.out.println("Saving to: " + newImgFile);
ImageIO.write(distorted, "png", newImgFile);
// Since we started a JavaFX thread in distortImg we have to shut it down. Otherwise the JVM won't exit
Platform.exit();
}
/**
* Applies perspective transformations to a copy of this {#code image} and rotates it.
* <p>
* Since this method starts a JavaFX thread, it's important to call {#link Platform#exit()} at the end of
* your application. Otherwise the thread will prevent the JVM from shutting down.
*
* #param image the image we want to distort
* #return the distorted image
*/
private static BufferedImage distortImg(BufferedImage image) {
// Necessary to initialize the JavaFX platform and to avoid "IllegalStateException: Toolkit not initialized"
new JFXPanel();
// This array allows us to get the distorted image out of the runLater closure below
final BufferedImage[] imageContainer = new BufferedImage[1];
// We use this latch to await the end of the JavaFX thread. Otherwise this method would finish before
// the thread creates the distorted image
final CountDownLatch latch = new CountDownLatch(1);
// To avoid "IllegalStateException: Not on FX application thread" we start a JavaFX thread
Platform.runLater(() -> {
int width = image.getWidth();
int height = image.getHeight();
Canvas canvas = new Canvas(width, height);
GraphicsContext graphicsContext = canvas.getGraphicsContext2D();
ImageView imageView = new ImageView(SwingFXUtils.toFXImage(image, null));
PerspectiveTransform trans = new PerspectiveTransform();
trans.setUlx(0);
trans.setUly(height / 4);
trans.setUrx(width);
trans.setUry(0);
trans.setLrx(width);
trans.setLry(height);
trans.setLlx(0);
trans.setLly(height - height / 2);
imageView.setEffect(trans);
imageView.setRotate(2);
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
Image newImage = imageView.snapshot(params, null);
graphicsContext.drawImage(newImage, 0, 0);
imageContainer[0] = SwingFXUtils.fromFXImage(newImage, image);
// Work is done, we decrement the latch which we used for awaiting the end of this thread
latch.countDown();
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return imageContainer[0];
}
}

making jfilechooser show image thumbnails

I wanted to create a JFileChooser with thumbnail view of image files.So I subclassed FileView and in the method which creates ImageIcon did some scaling sothat thumbnail images are shown.
However,the overall effect is that, the filechooser widget takes some time before opening a directory and showing thumbnails..In createImageIcon() below,I need to call new ImageIcon() twice-once with the image filepath and next with the resized image as constructor argument.I think this is what slows the widget .
Is there a more efficient alternative?Any suggestions/pointers most welcome.
thanks,
mark
public static void main(String[] args) {
JFileChooser chooser=new JFileChooser();
ThumbNailView thumbView=new ThumbNailView();
chooser.setFileView(thumbView);
}
class ThumbNailView extends FileView{
public Icon getIcon(File f){
Icon icon=null;
if(isImageFile(f.getPath())){
icon=createImageIcon(f.getPath(),null);
}
return icon;
}
private ImageIcon createImageIcon(String path,String description) {
if (path != null) {
ImageIcon icon=new ImageIcon(path);
Image img = icon.getImage() ;
Image newimg = img.getScaledInstance( 16, 16, java.awt.Image.SCALE_SMOOTH ) ;
return new ImageIcon(newimg);
} else {
System.err.println("Couldn't find file: " + path);
return null;
}
}
private boolean isImageFile(String filename){
//return true if this is image
}
I was actually surprised to see that, despite using the native look & feel in Windows, the file chooser indeed doesn't have a thumbnail view. I tried your example and you're going along the right lines, but I see how slow it was for folders with a lot of large images. The overhead is, of course, due to I/O when reading the file contents and then interpreting the image, which is unavoidable.
What's even worse, is that I found out that FileView.getIcon(File) is called a lot - before the file list is shown, when you mouse over an icon, and when the selection changes. If we don't cache the images after loading them, we'll be pointlessly reloading images all the time.
The obvious solution is to push all the image loading off onto another thread or a thread pool, and once we have our scaled-down result, put it into a temporary cache so it can be retrieved again.
I played around with Image and ImageIcon a lot and I discovered that an ImageIcon's image can be changed at any time by calling setImage(Image). What this means for us is, within getIcon(File), we can immediately return a blank or default icon, but keep a reference to it, passing it along to a worker thread that will load the image in the background and set the icon's image later when it's done (The only catch is that we must call repaint() to see the change).
For this example, I'm using an ExecutorService cached thread pool (this is the fastest way to get all images, but uses a lot of I/O) to process the image loading tasks. I'm also using a WeakHashMap as the cache, to ensure that we only hold onto the cached icons for as long as we need them. You could use another kind of Map, but you would have to manage the number of icons you hold onto, to avoid running out of memory.
package guitest;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.filechooser.FileView;
public class ThumbnailFileChooser extends JFileChooser {
/** All preview icons will be this width and height */
private static final int ICON_SIZE = 16;
/** This blank icon will be used while previews are loading */
private static final Image LOADING_IMAGE = new BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_INT_ARGB);
/** Edit this to determine what file types will be previewed. */
private final Pattern imageFilePattern = Pattern.compile(".+?\\.(png|jpe?g|gif|tiff?)$", Pattern.CASE_INSENSITIVE);
/** Use a weak hash map to cache images until the next garbage collection (saves memory) */
private final Map imageCache = new WeakHashMap();
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
JFileChooser chooser = new ThumbnailFileChooser();
chooser.showOpenDialog(null);
System.exit(1);
}
public ThumbnailFileChooser() {
super();
}
// --- Override the other constructors as needed ---
{
// This initializer block is always executed after any constructor call.
setFileView(new ThumbnailView());
}
private class ThumbnailView extends FileView {
/** This thread pool is where the thumnnail icon loaders run */
private final ExecutorService executor = Executors.newCachedThreadPool();
public Icon getIcon(File file) {
if (!imageFilePattern.matcher(file.getName()).matches()) {
return null;
}
// Our cache makes browsing back and forth lightning-fast! :D
synchronized (imageCache) {
ImageIcon icon = imageCache.get(file);
if (icon == null) {
// Create a new icon with the default image
icon = new ImageIcon(LOADING_IMAGE);
// Add to the cache
imageCache.put(file, icon);
// Submit a new task to load the image and update the icon
executor.submit(new ThumbnailIconLoader(icon, file));
}
return icon;
}
}
}
private class ThumbnailIconLoader implements Runnable {
private final ImageIcon icon;
private final File file;
public ThumbnailIconLoader(ImageIcon i, File f) {
icon = i;
file = f;
}
public void run() {
System.out.println("Loading image: " + file);
// Load and scale the image down, then replace the icon's old image with the new one.
ImageIcon newIcon = new ImageIcon(file.getAbsolutePath());
Image img = newIcon.getImage().getScaledInstance(ICON_SIZE, ICON_SIZE, Image.SCALE_SMOOTH);
icon.setImage(img);
// Repaint the dialog so we see the new icon.
SwingUtilities.invokeLater(new Runnable() {public void run() {repaint();}});
}
}
}
Known issues:
1) We don't maintain the image's aspect ratio when scaling. Doing so could result in icons with strange dimensions that will break the alignment of the list view. The solution is probably to create a new BufferedImage that is 16x16 and render the scaled image on top of it, centered. You can implement that if you wish!
2) If a file is not an image, or is corrupted, no icon will be shown at all. It looks like the program only detects this error while rendering the image, not when we load or scale it, so we can't detect this in advance. However, we might detect it if we fix issue 1.
Use fileDialog instead of JfileChooser for choising the image:
FileDialog fd = new FileDialog(frame, "Test", FileDialog.LOAD);
String Image_path
fd.setVisible(true);
name = fd.getDirectory() + fd.getFile();
image_path=name;
ImageIcon icon= new ImageIcon(name);
icon.setImage(icon.getImage().getScaledInstance(jLabel2.getWidth(),jLabel2.getHeight() , Image.SCALE_DEFAULT));
jLabel2.setIcon(icon);
You could use a default icon for each fileand load the actual icons in another thread (perhaps using a SwingWorker?). As the icons are loaded the SwingWorker could call back and update the FileView.
Not sure if a single SwingWorker would do the trick, or whether it would be better to use one for each icon being loaded.

Categories

Resources