CollorAdjust with SwingX's ImageView? - java

I have a question about adjusting contrast, saturation and hue of an image that's loaded to jXImageView from swingx library.
I have the ColorAdjust methods.
ColorAdjust colorAdjust = new ColorAdjust();
colorAdjust.setContrast(0.3);
colorAdjust.setHue(-0.03);
colorAdjust.setBrightness(0.2);
colorAdjust.setSaturation(0.2);
When the user click on the "Enhancement" button, the image should change a bit, but how to do that? Remember: I'm using the jXImageView.
I've increased the contrast already by using this code:
float brightenFactor = 1.5f;
BufferedImage imagem = (BufferedImage) jXImageView2.getImage();
RescaleOp op = new RescaleOp(brightenFactor, 0, null);
imagem = op.filter(imagem, imagem);
jXImageView2.updateUI();
Edit
I tryied:
BufferedImage imagem = (BufferedImage) jXImageView2.getImage();
Image image = SwingFXUtils.toFXImage(imagem, null);//<--ERROR on that line (incompatible types: writable image cannot be converted to Image)
ColorAdjust colorAdjust = new ColorAdjust();
colorAdjust.setContrast(0.3);
colorAdjust.setHue(-0.03);
colorAdjust.setBrightness(0.2);
colorAdjust.setSaturation(0.2);
ImageView imageView = new ImageView(image);//<--ERROR on taht line no suitable constructor for ImageView(java.awt.Image)
imageView.setFitWidth(imagem.getWidth());
imageView.setPreserveRatio(true);
imagem = SwingFXUtils.fromFXImage(imageView.snapshot(null, null), null);
jXImageView2.setImage(imagem);
...but without successful.

Sample solution
Image on the left is the original image.
Image on the right is the adjusted image (which has had the color desaturated to make the image monochrome).
This solution works by:
Converting the Swing/AWT BufferedImage into a JavaFX Image.
Using the JavaFX ColorAdjust effect to modify the image.
A snapshot of the color adjusted image is taken to create a new JavaFX image.
The new JavaFX image is converted back to a new Swing/AWT BufferedImage.
Because the solution mixes two different toolkits, the following considerations were applied when creating it:
Be careful of imports used to ensure that the correct class is being used for a given toolkit call; e.g., both JavaFX and Swing/AWT have Color and Image classes, so it is necessary to ensure that the fully qualified class for a given toolkit is used in the right context - passing a Swing Image directly to a JavaFX API would be wrong and vice-versa.
Be careful of threading rules. Snapshots of JavaFX scenes must be made on the JavaFX application thread. Execution of Swing APIs must be made on the Swing event dispatch thread. Various utilities of the respective toolkits (e.g., SwingUtilities and the JavaFX Platform class) are used to ensure threading constraints of the given toolkits are satisfied.
The JavaFX toolkit must be initialized before it can be used. Normally this is done implicitly when your application extends the JavaFX Application class. However Swing applications do not extend the JavaFX application class. So, perhaps somewhat counter-intuitively and poorly documented, a JFXPanel must be instantiated to initialize the JavaFX toolkit before the toolkit is used.
Notes
This solution is crafted to fit the particular requirements of the question (which is a Swing application which needs to make some color adjustments). If you only wish to adjust image colors from within JavaFX and not use Swing, then more straight-forward solutions exist and are preferred.
Calling System.exit is generally enough to shut the JavaFX toolkit down. The sample application calls Platform.exit to explicitly shut the JavaFX toolkit down, but in this case the explicit call to Platform.exit is probably unnecessary.
This means that the ColorAdjuster in the solution can be used from a Swing program without the Swing program explicitly importing any JavaFX classes (although, internally, the ColorAdjuster will import those classes and the system must meet the normal minimum requirements to run both the Swing and JavaFX toolkits). Reducing mixing of imports to a single toolkit per class where possible is desirable because mixing imports within a single class for a mixed JavaFX/Swing application is a good source of tedious errors, due to potential name clashes and threading related headaches.
ColorAdjuster.java
Image color adjusting utility.
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.SnapshotParameters;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javax.swing.SwingUtilities;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/** Uses JavaFX to adjust the color of an AWT/Swing BufferedImage */
public class ColorAdjuster {
// Instantiation of a JFXPanel is necessary otherwise the JavaFX toolkit is not initialized.
// The JFXPanel doesn't actually need to be used, instantiating it in the constructor is enough to trigger toolkit initialization.
private final JFXPanel fxPanel;
public ColorAdjuster() {
// perhaps this check is not necessary, but I feel a bit more comfortable if it is there.
if (!SwingUtilities.isEventDispatchThread()) {
throw new IllegalArgumentException(
"A ColorAdjuster must be created on the Swing Event Dispatch thread. " +
"Current thread is " + Thread.currentThread()
);
}
fxPanel = new JFXPanel();
}
/**
* Color adjustments to the buffered image are performed with parameters in the range -1.0 to 1.0
*
* #return a new BufferedImage which has colors adjusted from the original image.
**/
public BufferedImage adjustColor(
BufferedImage originalImage,
double hue,
double saturation,
double brightness,
double contrast
) throws ExecutionException, InterruptedException {
// This task will be executed on the JavaFX thread.
FutureTask<BufferedImage> conversionTask = new FutureTask<>(() -> {
// create a JavaFX color adjust effect.
final ColorAdjust monochrome = new ColorAdjust(0, -1, 0, 0);
// convert the input buffered image to a JavaFX image and load it into a JavaFX ImageView.
final ImageView imageView = new ImageView(
SwingFXUtils.toFXImage(
originalImage, null
)
);
// apply the color adjustment.
imageView.setEffect(monochrome);
// snapshot the color adjusted JavaFX image, convert it back to a Swing buffered image and return it.
SnapshotParameters snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
return SwingFXUtils.fromFXImage(
imageView.snapshot(
snapshotParameters,
null
),
null
);
});
Platform.runLater(conversionTask);
return conversionTask.get();
}
}
ColorAdjustingSwingAppUsingJavaFX.java
Test harness:
import javafx.application.Platform;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
public class ColorAdjustingSwingAppUsingJavaFX {
private static void initAndShowGUI() {
try {
// This method is invoked on Swing thread
JFrame frame = new JFrame();
// read the original image from a URL.
URL url = new URL(
IMAGE_LOC
);
BufferedImage originalImage = ImageIO.read(url);
// use JavaFX to convert the original image to monochrome.
ColorAdjuster colorAdjuster = new ColorAdjuster();
BufferedImage monochromeImage = colorAdjuster.adjustColor(
originalImage,
0, -1, 0, 0
);
// add the original image and the converted image to the Swing frame.
frame.getContentPane().setLayout(new FlowLayout());
frame.getContentPane().add(
new JLabel(
new ImageIcon(originalImage)
)
);
frame.getContentPane().add(
new JLabel(
new ImageIcon(monochromeImage)
)
);
// set a handler to close the application on request.
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
// shutdown the JavaFX runtime.
Platform.exit();
// exit the application.
System.exit(0);
}
});
// display the Swing frame.
frame.pack();
frame.setLocation(400, 300);
frame.setVisible(true);
} catch (IOException | InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(
ColorAdjustingSwingAppUsingJavaFX::initAndShowGUI
);
}
// icon source: http://www.iconarchive.com/artist/aha-soft.html
// icon license: Free for non-commercial use, commercial usage: Not allowed
private static final String IMAGE_LOC =
"http://icons.iconarchive.com/icons/aha-soft/desktop-buffet/128/Pizza-icon.png";
}

You need to convert the BufferedImage to a javafx.scene.image.Image, you can use something like...
Image image = SwingFXUtils.toFXImage(imagem, null);
Then you can apply the ColorAdjust...
ColorAdjust colorAdjust = new ColorAdjust();
colorAdjust.setContrast(0.1);
colorAdjust.setHue(-0.05);
colorAdjust.setBrightness(0.1);
colorAdjust.setSaturation(0.2);
ImageView imageView = new ImageView(image);
imageView.setFitWidth(image.getWidth());
imageView.setPreserveRatio(true);
imageView.setEffect(colorAdjust);
Then convert it back again...
imagem = SwingFXUtils.fromFXImage(imageView.snapshot(null, null), null);
This idea is stolen from jewelsea / SaveAdjustedImage.java. What I don't know is, if the ImageView needs to be realised on the screen first all not...
Updated
Just so you are aware, you are crossing two different UI frameworks, like they say in the films, "don't cross the streams!"
JavaFX has a much more tightly controlled set of requirements then Swing does, this is both a good and bad thing.
What you MUST do, is get the JavaFX code to run within it's event thread. This is more tricky than it sounds (and seems to need to be), for example...
Original | Color adjustments (taken from the JavaDocs example) | Monochrome...
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
public class Test extends Application {
public static void main(String[] args) {
Application.launch();
}
#Override
public void start(Stage stage) throws Exception {
try {
System.out.println("Load image...");
BufferedImage imagem = ImageIO.read(new File("..."));
Image image = SwingFXUtils.toFXImage(imagem, null);
ColorAdjust colorAdjust = new ColorAdjust();
colorAdjust.setHue(0);
colorAdjust.setSaturation(-1);
colorAdjust.setBrightness(0);
colorAdjust.setContrast(0);
// colorAdjust.setHue(-0.05);
// colorAdjust.setSaturation(0.2);
// colorAdjust.setBrightness(0.1);
// colorAdjust.setContrast(0.1);
ImageView imageView = new ImageView(image);
imageView.setFitWidth(image.getWidth());
imageView.setPreserveRatio(true);
imageView.setEffect(colorAdjust);
System.out.println("Convert and save...");
imagem = SwingFXUtils.fromFXImage(imageView.snapshot(null, null), null);
ImageIO.write(imagem, "png", new File("ColorAdjusted.png"));
} catch (IOException exp) {
exp.printStackTrace();
} finally {
Platform.exit();
}
}
}
The next thing is trying to work out how you would get this to work as a utility class...

Related

Can I supply image icons in Java in a higher resolution to avoid blurred icons after scaling?

I am designing a GUI with Java Swing and AWT (Java 8) and am struggling with the icons I use.
I load a large PNG image and scale it to 18x18px and then use it in a button or label. It works well in all resolutions when the operating system does not zoom in.
However, with the advent of large screen resolutions (hidpi), it is common practice to use operating system settings to zoom in on user interface controls, including buttons and such things in Java applications. For example, on Windows I use a 150% or 200% scaling of user elements with my 4K resolution to ensure the user interface is still usable. I imagine many users will do so as well.
When that is the case, however, the icons are merely increased in size after already scaling them down to 18x18px. That is, I first scale them down and then the operating system tries to scale them up again with the little information that is still left in the image.
Is there any way to design image icons in Java that are based on a higher resolution when the zooming/scaling capabilities of the operating system are used in order to avoid them appearing blurred?
Here is a working example:
import java.awt.Container;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
#SuppressWarnings("serial")
class Example extends JFrame {
public static void main(String[] args) {
new Example();
}
public Example() {
Container c = getContentPane();
JPanel panel = new JPanel();
ImageIcon icon = new ImageIcon(new ImageIcon(getClass().getResource("tabler-icon-beach.png")).getImage().getScaledInstance(18, 18, Image.SCALE_SMOOTH));
JButton button = new JButton("Test button", icon);
panel.add(button);
c.add(panel);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
}
You can find the icon here. All icons are available as PNG or SVG files.
To illustrate the problem, let me first show you two screenshots in the normal 100% screen resolution:
On Linux with 100% zoom:
On Windows with 100% zoom:
And now when I set Windows 7 to have a 200% magnification of layout elements, it's obviously just the 18x18px version stretched out, which becomes blurred:
Is there any way to provide a higher-resolution image icon that is used when the operating system uses a scaling that is larger than 100%? Moreover, you can see that even at 100% the image quality is not perfect; is there any way to improve that as well?
Java 8 does not support High DPI, the UI gets scaled up by Windows. You should use Java 11 or a later version which support per-monitor High DPI settings.
If your goal is to make the icons look crisp, prepare a set of icons for different resolutions using BaseMultiResolutionImage (the basic implementation of MultiResolutionImage) to provide higher resolution alternatives. (These are not available in Java 8.)
You say that you scaled down the original image (240×240) to 18×18px. If the UI needs a higher resolution according to the system setting, all it has now is your small icon (18×18) which will be scaled up, which results in poor quality. You should use a MultiResolutionImage or paint the original image into the required size, letting Graphics to scale it down for you.
No Down-Scale
This is the simplest way I came up with to make the icon 18×18 without downscaling the original image:
private static final String IMAGE_URL =
"https://tabler-icons.io/static/tabler-icons/icons-png/beach.png";
private static ImageIcon getIcon() {
return new ImageIcon(Toolkit.getDefaultToolkit()
.getImage(new URL(IMAGE_URL))) {
#Override
public int getIconWidth() {
return 18;
}
#Override
public int getIconHeight() {
return 18;
}
#Override
public synchronized void paintIcon(Component c, Graphics g,
int x, int y) {
g.drawImage(getImage(), x, y, 18, 18, null);
}
};
}
I left out the exception handling code for MalformedURLException which can be thrown from the URL constructor.
In this case, the painted image gets down-scaled each time it's painted, which is ineffective. Yet the quality is better. Well, for the standard resolution screen, it's nearly the same as if you down-scaled the image when loading. But in High DPI case, it looks better. It's because for 200% UI Scale, the image will be rendered to 36×36 pixels and these pixels will be created from the source of 240×240 rather than up-scaling the down-scaled version which lost its quality.
To get even better results, I recommend using MultiResolutionImage.
MultiResolutionImage
The app below loads the images from base64-encoded strings (for simplicity so that there are no external dependencies). There are three variants provided: 24×24 (100%, 96dpi), 36×36 (150%, 144dpi), 48×48 (200%, 192dpi).
If the current scale factor is set to any of the provided resolutions, the image will be rendered as is. If 125% or 175% are used, the larger image will be scaled down; if the scale is greater than 200%, then the image for 200% will be scaled up. You can add more resolutions if needed.
The app doesn't compile in Java 8 because MultiResolutionImage is not available there. To compile it with JDK 11, you have to replace text blocks with regular String concatenation.
import java.awt.Image;
import java.awt.image.BaseMultiResolutionImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class BeachIconButton {
public static void main(String[] args) {
SwingUtilities.invokeLater(BeachIconButton::new);
}
private BeachIconButton() {
JPanel panel = new JPanel();
ImageIcon icon = getIcon();
JButton button = new JButton("Test button", icon);
panel.add(button);
JFrame frame = new JFrame("Beach Icon Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static ImageIcon getIcon() {
return new ImageIcon(
new BaseMultiResolutionImage(
Arrays.stream(new String[] { BEACH_100, BEACH_150, BEACH_200})
.map(BeachIconButton::loadImage)
.toArray(Image[]::new)));
}
private static Image loadImage(String base64) {
try {
return ImageIO.read(new ByteArrayInputStream(
Base64.getMimeDecoder().decode(base64)));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private static final String BEACH_100 = """
iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAA7DAAAO
wwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAXxJ
REFUSInl1E9LVVEUBfAfVJBIIoWGIDiRGtagN7Yg+gTZXPwICU4FyUGDahBRCTrK
qUVNIxrkJHjWJMKZikIjSyHoYd0GZ186vOef7n2zWnB4e591WXudt/c5/G8Ywiw+
4gd28QGLuNCt+C18RXHI+onJuuLjIVBgGWOYyPb2syI3q4qfzZw/ir1raMXeNBay
k+zgfNUC25nTBWxGPhfffI78XfzOVD1FP+5lrgusoweXIv+C6xE3qxYocRFrWZHv
eBPxA5yJeK9ugRJPdU7QGPoi3+1GfFBqZIFn+BVxC88jft9NgdL9q8gH8MSfUa3V
5BKXQ6gl9aOdey1N2GBV4VO4ik/h8CWG67rMcQ53Hf5ErOBGXfEGtjKxVSxhHi/w
LeMe4mQV8ROZ67fSZWrHadyW7kKBqaonuC89Bcc5uyKNZ+M4wTvS9d/AY/RmXK80
iht/wa+HVgfKB61cTYzGaups8FH89kEFhjBSUzDnR0LrSFT5Sw7i/yH8BmQ0mnmX
f2wqAAAAAElFTkSuQmCC""";
private static final String BEACH_150 = """
iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAACXBIWXMAABYlAAAW
JQFJUiTwAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAoVJ
REFUWIXt1kuoTVEYB/DfdZWiq7gykZBHHokyw0QGREpKLgMDGSojQ6+JARFi4JFn
ySPyyoCJR8zcIUVJiPLopiMhLoO1d3ftc+7Z95xrnYHyr1V7feu/1/ff3/etby/+
4x9DW8K9ZmE9FmTP7fiBp3iE03iS0F9dTMQN/G5gvMLMVopZhi8NisnHD6xohZiV
2ea5o584h+WYgAclor5jaUoxE9ATOXiOednaUMUU9mKPkK5YVCXbJwkuKkZmYbS2
t8rxpsz+QW2krqYSdF5tCnZjCX5F9h0Zf25k66nizEghaBSOV22cRyt/vhnxd0b2
E7gQzXelEJRjnv6L96dQ9IR+9CxaWymcsnx+L6UgQnPtUizyfDwRaiiff8JwjIts
71MLytGBy0I9xaLitG7LuCMi2+d4kyEJBVWwWkhPfz568UaI6Mho/XtCDTXYqO/L
v+GF2jQ+wNZofrdVYjrwNnK0XYjOYSEt9bp20lMWY0/k5LVQJzlGCn2qur56Mb0V
YiYLKcodddXhTcX1iJesU1cj/nc9NPA9axE2Cw02GUYITe6UYgoOCv+29pTOyjAH
VxRT1N/4iP0Y2yohHTij9v810Khgiwavy43eqSfhGmZHtt/oxm2h4fUI0Zim75IW
4wI24GuDPutiCF4q1sklAx/XJXisGK2zfysGhum7N3/F2ibebce+SNCdFIJgMQ4J
F63BYBUOYEqzL87HLaEeKrivPCLrMk5lEPyezNf8MkHv9H9STioegDbF/vM3/Hdl
grpLnBxFJ8bgWAmvWX53maBOId/jMRpHGnB8JOM2yx+f+eosE1SNNiH89TY/oTY1
zfAHjS7hMv45G3exJiH/P/5d/AHE21JDZYKOHAAAAABJRU5ErkJggg==""";
private static final String BEACH_200 = """
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAB2HAAAd
hwGP5fFlAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA5FJ
REFUaIHt2ctrXFUcB/BPYoWGVGOwBZtYQQoqhUp9IFEoulC04spGExQEpYoiVBBc
qVAVwYX/gEWqooKboItqN4oV0frWPgR3XRSl9mHT2GjaJh0X5w6ZOXPuzO08roL5
wlnM+T3O93cev3vOb1jCEv7f6CtpnBswjg1Yh2HM4wx+wh58jC9K4lMIfXgIB1Ap
2H7FC7jwX+Bbh2uwW3HiqUDWlE26io040YJgkXYKd5TM3Z34K0HmNKYwgavxgLD/
WwUxi1vKIr8GfyRIfIAravTuxtmE3g+Jvkrm86pek+/XuOfP4elIbz1mIr15bBUm
IG8lPtHjjLkpMeg+jNboDOKXSOcsJjP5bU0CqODBsgOoHsRnsRyvJ+RP1vh4PpId
in7v7WUA/diRE0QFhxN92yMfeyL5MxoP+nW9DAJuxY85QdS2AxiosVsrnJmqfAGX
4/3I7uVeBwAX4DEcaRLAFFbV2LwSyXdn/ZNR/86es6/BCmyTTpnVWX4Tl2pMv49k
PtZF/QfLIl+L9fhN/mrEK3XE4vYajGQzZRKPcTuOyw+k2rbV2FwSyY7GTvt7yTjC
UQwV0FspEKf+nMBcVxmdJz7VeEVYkF6F43gKD0f9H5bOOsN9Gkluws04lpBV21z0
+7myiRO+wgcjInE6HE/opNpYOZTrEV8RzghX6hjLhWvHKWny35RBNsZogtCrLWxG
8JrG8zHRO5r5eCci8bvFDNMKY/hMeNRsF77wpWJM/f2mgke7PciyLvtbKby4NuJe
9Y+QGdyEv7FLSJX/CfThfqGmM691Jqlkep8L6bWs2lQSd+F7xUjnte+EYkCpGMQb
HRKP247M73mhneVbhY9wY0J2TpjRnUKV4bBQIxrGalyPe4RSY+oe9q1who61wasQ
+vC1xtlbwFu4sqCftXhb+i70lR6ei+HEgPtxbZv+NuDnhM/hjpnmoE+YoepAu7Sx
byOsELZkWyvQzlINCTWaGbwnpMROsUx4/16Md3GyE2cDQqXsS0wLRPfhJfWFqjyM
Zrr78WcX7KczLlvVVzJyMSU/1U1brKSlMJnp9Mp+qkgAs00cVL+imxN244p9iTux
ny0SQLMVqLY5PCHs2SGhRHi6gF2n9oVWYABbhOLUZbgoGyx+3rUi+Hhm26n96ozL
FgXPQB4262yLdGrfFUxo/vfRCc1fTp3adwUjeFEod5/M2l7h38WREuyXsIQl1OAf
9zFZ1uiy3BkAAAAASUVORK5CYII=""";
}

Blurry render of SwingNode in JavaFX on Windows

Overview
Using FlyingSaucer within a JavaFX application, to avoid WebView for various reasons:
doesn't provide direct API access to its scrollbars for synchronous behaviour;
bundles JavaScript, which is a huge bloat for my use case; and
failed to run on Windows.
FlyingSaucer uses Swing, which requires wrapping its XHTMLPanel (a subclass of JPanel) in a SwingNode to use alongside JavaFX. Everything works great, the application renders Markdown in real-time, and is responsive. Here's a demo video of the application running on Linux.
Problem
The text rendering on Windows is blurry. When running in a JFrame, not wrapped by a SwingNode, but still part of the same application shown in the video, the quality of the text is flawless. The screen capture shows the application's main window (bottom), which includes the SwingNode along with the aforementioned JFrame (top). You may have to zoom into the straight edge of the "l" or "k" to see why one is sharp and the other blurry:
This only happens on Windows. When viewing the font on Windows through the system's font preview program, the fonts are antialiased using LCD colours. The application uses grayscale. I suspect that if there is a way to force the rendering to use colour for antialiasing instead of grayscale, the problem may disappear. Then again, when running within its own JFrame, there is no problem and LCD colours are not used.
Code
Here's the code for the JFrame that has a perfect render:
private static class Flawless {
private final XHTMLPanel panel = new XHTMLPanel();
private final JFrame frame = new JFrame( "Single Page Demo" );
private Flawless() {
frame.getContentPane().add( new JScrollPane( panel ) );
frame.pack();
frame.setSize( 1024, 768 );
}
private void update( final org.w3c.dom.Document html ) {
frame.setVisible( true );
try {
panel.setDocument( html );
} catch( Exception ignored ) {
}
}
}
The code for the blurry SwingNode is a little more involved (see full listing), but here are some relevant snippets (note that HTMLPanel extends from XHTMLPanel only to suppress some undesired autoscrolling during updates):
private final HTMLPanel mHtmlRenderer = new HTMLPanel();
private final SwingNode mSwingNode = new SwingNode();
private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
// ...
final var context = getSharedContext();
final var textRenderer = context.getTextRenderer();
textRenderer.setSmoothingThreshold( 0 );
mSwingNode.setContent( mScrollPane );
// ...
// The "preview pane" contains the SwingNode.
final SplitPane splitPane = new SplitPane(
getDefinitionPane().getNode(),
getFileEditorPane().getNode(),
getPreviewPane().getNode() );
Minimal Working Example
Here's a fairly minimal self-contained example:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingNode;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.xhtmlrenderer.simple.XHTMLPanel;
import javax.swing.*;
import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.UIManager.getSystemLookAndFeelClassName;
import static javax.swing.UIManager.setLookAndFeel;
public class FlyingSourceTest extends Application {
private final static String HTML = "<!DOCTYPE html><html><head" +
"><style type='text/css'>body{font-family:serif; background-color: " +
"#fff; color:#454545;}</style></head><body><p style=\"font-size: " +
"300px\">TEST</p></body></html>";
public static void main( String[] args ) {
Application.launch( args );
}
#Override
public void start( Stage primaryStage ) {
invokeLater( () -> {
try {
setLookAndFeel( getSystemLookAndFeelClassName() );
} catch( Exception ignored ) {
}
primaryStage.setTitle( "Hello World!" );
final var renderer = new XHTMLPanel();
renderer.getSharedContext().getTextRenderer().setSmoothingThreshold( 0 );
renderer.setDocument( new W3CDom().fromJsoup( Jsoup.parse( HTML ) ) );
final var swingNode = new SwingNode();
swingNode.setContent( new JScrollPane( renderer ) );
final var root = new SplitPane( swingNode, swingNode );
// ----------
// Here be dragons? Using a StackPane, instead of a SplitPane, works.
// ----------
//StackPane root = new StackPane();
//root.getChildren().add( mSwingNode );
Platform.runLater( () -> {
primaryStage.setScene( new Scene( root, 300, 250 ) );
primaryStage.show();
} );
} );
}
}
Blurry capture from the minimal working example;
zooming in reveals letter edges are heavily antialiased rather than sharp contrasts:
Using a JLabel also exhibits the same fuzzy render:
final var label = new JLabel( "TEST" );
label.setFont( label.getFont().deriveFont( Font.BOLD, 128f ) );
final var swingNode = new SwingNode();
swingNode.setContent( label );
Attempts
Here are most of the ways I've tried to remove the blur.
Java
On the Java side, someone suggested to run the application using:
-Dawt.useSystemAAFontSettings=off
-Dswing.aatext=false
None of the text rendering hints have helped.
Setting the content of the SwingNode within SwingUtilities.invokeLater has no effect.
JavaFX
Someone else mentioned that turning caching off helped, but that was for a JavaFX ScrollPane, not one within a SwingNode. It didn't work.
The JScrollPane contained by the SwingNode has its alignment X and alignment Y set to 0.5 and 0.5, respectively. Ensuring a half-pixel offset is recommended elsewhere. I cannot imagine that setting the Scene to use StrokeType.INSIDE would make any difference, although I did try using a stroke width of 1 to no avail.
FlyingSaucer
FlyingSaucer has a number of configuration options. Various combinations of settings include:
java -Dxr.text.fractional-font-metrics=true \
-Dxr.text.aa-smoothing-level=0 \
-Dxr.image.render-quality=java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC
-Dxr.image.scale=HIGH \
-Dxr.text.aa-rendering-hint=VALUE_TEXT_ANTIALIAS_GASP -jar ...
The xr.image. settings only affect images rendered by FlyingSaucer, rather than how the output from FlyingSaucer is rendered by JavaFX within the SwingNode.
The CSS uses points for the font sizes.
Research
https://stackoverflow.com/a/26227562/59087 -- looks like a few solutions may be helpful.
https://bugs.openjdk.java.net/browse/JDK-8089499 -- doesn't seem to apply because this is using SwingNode and JScrollPane.
https://stackoverflow.com/a/24124020/59087 -- probably not relevant because there is no XML scene builder in use.
https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf -- page 8 describes shifting by 0.5 pixels, but how?
https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ -- suggests not mixing JavaFX and Swing, but moving to pure Swing isn't an option: I'd sooner rewrite the app in another language.
Accepted as a bug against OpenJDK/JavaFX:
https://bugs.openjdk.java.net/browse/JDK-8252255
JDK & JRE
Using Bellsoft's OpenJDK with JavaFX bundled. To my knowledge, the OpenJDK has had Freetype support for a while now. Also, the font looks great on Linux, so it's probably not the JDK.
Screen
The following screen specifications exhibit the problem, but other people (viewing on different monitors and resolutions, undoubtedly) have mentioned the issue.
15.6" 4:3 HD (1366x768)
Full HD (1920x1080)
Wide View Angle LED Backlight
ASUS n56v
Question
Why does FlyingSaucer's XHTMLPanel when wrapped within SwingNode become blurry on Windows, and yet displaying the same XHTMLPanel in a JFrame running in the same JavaFX application appears crisp? How can the problem be fixed?
The problem involves SplitPane.
There are a few options that you might try although I have to admit that I do not know FlyingSaucer and its API.
FlyingSaucer has different renderers. Thus it might be possible to avoid the Swing/AWT rendering completely by using this library instead in order to do all the rendering directly in JavaFX. https://github.com/jfree/fxgraphics2d
Another possibility is to let FlyingSaucer render into an image which can the be displayed in JavaFX very efficiently via direct buffers. See the AWTImage code in my repository here: https://github.com/mipastgt/JFXToolsAndDemos
I wasn't able to reproduce the issue myself, so there may be some issue in the combination of JDK/JavaFX version you are using. It's also possible that the issue only arises with a specific combination of display size and screen scaling.
My setup is the following:
JavaFX 14
OpenJDK 14
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingNode;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.jsoup.nodes.Document;
import org.xhtmlrenderer.simple.XHTMLPanel;
import javax.swing.*;
public class FlyingSourceTest extends Application {
private final static String HTML_PREFIX = "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<body>\n";
private static final String HTML_CONTENT =
"<p style=\"font-size:500px\">TEST</p>";
private final static String HTML_SUFFIX = "<p style='height=2em'> </p></body></html>";
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
SwingUtilities.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
primaryStage.setTitle("Hello World!");
XHTMLPanel mHtmlRenderer = new XHTMLPanel();
mHtmlRenderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
SwingNode mSwingNode = new SwingNode();
JScrollPane mScrollPane = new JScrollPane(mHtmlRenderer);
String htmlContent = HTML_PREFIX + HTML_CONTENT + HTML_SUFFIX;
Document jsoupDoc = Jsoup.parse(htmlContent);
org.w3c.dom.Document w3cDoc = new W3CDom().fromJsoup(jsoupDoc);
mHtmlRenderer.setDocument(w3cDoc);
mSwingNode.setContent(mScrollPane);
// AnchorPane anchorPane = new AnchorPane();
// anchorPane.getChildren().add(mSwingNode);
// AnchorPane.setTopAnchor(mSwingNode, 0.5);
// AnchorPane.setLeftAnchor(mSwingNode, 0.5);
// mSwingNode.setTranslateX(0.5);
// mSwingNode.setTranslateY(0.5);
StackPane root = new StackPane();
root.getChildren().add(mSwingNode);
Platform.runLater(() -> {
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
});
});
}
}
The issue has been accepted as a bug against OpenJDK/JavaFX:
https://bugs.openjdk.java.net/browse/JDK-8252255
Neither of Mipa's suggestions would work in practice. FlyingSaucer is tightly integrated with a JScrollPane, which precludes the possibility of forcing FlyingSaucer to render onto a JavaFX-based panel.
Another possibility is to go the opposite direction: create a Swing application and embed JavaFX controls, such as using a JFXPanel; however, it would seem more prudent to accept the blurry behaviour until the bug is bashed.

Java Applet, make the background transparent

I watched a tutorial on YouTube on how to display an image in Java, and he used something called "Applet" and "Graphics" to display it, I've got it to work, and I am happy with it. Now what I was planning to make is a chrome logo in the center of my screen, transparent with no background, and then chrome opens. I've called the class CustomChrome cus you know. Custom and stuff. I just want a cool opening whenever I start chrome.
Here is the current code:
import java.applet.Applet;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.net.URL;
public class CustomChrome extends Applet {
/**
*
*/
private static final long serialVersionUID = 1L;
private Image logo = null;
public void paint(Graphics g) {
this.setSize(960, 540);
if (logo == null)
logo = getImage("/chromelgo.png");
Graphics2D g2 = (Graphics2D) g;
g2.drawImage(logo, 0, 0, 960, 540, this);
}
public Image getImage(String path) {
Image tempImage = null;
try {
URL imageURL = CustomChrome.class.getResource(path);
tempImage = Toolkit.getDefaultToolkit().getImage(imageURL);
} catch (Exception e) {
System.out.println("An error occured - " + e.getMessage());
}
return tempImage;
}
}
What I want to do, however, is the make the background + the window to vanish, Once that's was done I will set the image and window size to 1920x1080. How do I go forward on making the window behind the image disappear? I've heard something about implementing ActionListener, but still I ain't sure what to do.
Keep in mind! I have no experience with java so sorry for upsetting you if you try to help me :P
Okay, let's start with the really obvious issue, applets are effectively deprecated and you should stop using them, see Java Plugin support deprecated and Moving to a Plugin-Free Web for more details.
paintXxx is for painting, you should never change the state of a component from within a paint method (and you should not be calling setSize in an applet anyway). See Painting in AWT and Swing and Performing Custom Painting for more details about how painting works and how you can make use of it
As a general piece of advice, I'd recommend ImageIO.read over Toolkit#getImage or ImageIcon for loading images, apart from throwing an Exception when the image can't be read (instead of failing silently), it's also a blocking call, meaning that when it returns, the image is fully loaded.
See Reading/Loading an Image for more details
A word of warning - What you're trying to do is not difficult per se, but it's involved and requires a certain amount of knowledge about how the API works.
Now what I was planning to make is a chrome logo in the center of my screen, transparent with no background, and then chrome opens.
Okay, well, this was never going to work with applets, as applets are intended to be displayed inside a browser, so you can't control the window, instead, you need something like a JFrame, see How to Make Frames (Main Windows)
What I want to do, however, is the make the background + the window to vanish, Once that's was done I will set the image and window size to 1920x1080. How do I go forward on making the window behind the image disappear?
Now, this is where the real fun begins. You're going to want to start by having a look at How to Create Translucent and Shaped Windows. This will allow you control the opacity of the frame, making it disappear, but allowing the content to remain, there are some tricks you need to do to pull it off, but that's the basic idea
So, this example will make a transparent window and display and image centered within it.
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
BufferedImage img = ImageIO.read(getClass().getResource("/Chrome.png"));
ImageIcon icon= new ImageIcon(img);
JLabel label = new JLabel(icon);
JFrame frame = new JFrame("Testing");
frame.setUndecorated(true);
frame.setBackground(new Color(0, 0, 0, 0));
frame.add(label);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
}
In this is example, I make use of a JLabel to display the image, in most cases, it's all you really need, see How to Use Labels for more details.
What I want to do, however, is the make the background + the window to vanish
Well, it just comes down to what you mean by vanish. You could just all dispose or setVisible on the window, which would close it, all you could use some animation to make it fade away ... and because I like animation...
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
BufferedImage img = ImageIO.read(getClass().getResource("/Chrome.png"));
ImageIcon icon = new ImageIcon(img);
JLabel label = new JLabel(icon);
JFrame frame = new JFrame("Testing");
frame.setUndecorated(true);
frame.setBackground(new Color(0, 0, 0, 0));
frame.add(label);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Timer timer = new Timer(40, new ActionListener() {
float opacity = 1.0f;
float delta = 0.05f;
#Override
public void actionPerformed(ActionEvent e) {
opacity -= delta;
if (opacity < 0.0f) {
opacity = 0.0f;
((Timer)(e.getSource())).stop();
frame.dispose();
}
frame.setOpacity(opacity);
}
});
timer.setInitialDelay(2000);
timer.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
}
Okay, there is a lot going on here, you'll want to have a read through Concurrency in Swing and How to use Swing Timers for more details, needless to say, it's a little complicated to get started with.
Basically, all this does is, waits 2 seconds and then every 40 milliseconds, decreases the frames opacity by a factor or 0.05 till it reaches 0 and it then disposes of the window.
Now, I might use a mixture of these techniques, for example, I might show the logo frame (maybe set to display on top), I would then show the main frame and then trigger the logo frame to fade out. This could be done from a controller class to simplify the code, but, that's up to you.
A would also suggest having a look at Creating a GUI With JFC/Swing

Draw high quality HTML to Graphics2D (analogous to drawString())

I know how to draw HTML to a Graphics2D object using Swing's limited built-in HTML support (see http://www.java.net/node/680674), but I need better rendering. Most importantly for this particular chemistry diagram drawing application, Swing's HTML support does not extend to nested sub/superscripts. Better CSS support would be nice too. I don't need image embedding or interactive features such as Javascript or hotlinks.
The HTML text is scaled and rotated and then drawn into a diagram that presumably contains additional text and graphics. The Graphics2D target may be the screen, a printer, or (via iText) a PDF file. I doubt that any solution involving conversion via a BufferedImage or the like can be adequately compact when producing PDF files of publication quality.
My (possibly incorrect) impression is that JavaFX 2.0 does not yet have a solution to this, though it might eventually. (If an earlier version can do this, that might be a solution.) Rewriting the entire application from Swing to JavaFX is not realistic.
This application is free and open source, so any tool it uses probably needs to be freely distributable also. Otherwise, I believe JWebEngine might have fit the bill.
Any help would be appreciated.
You could use a JavaFX WebView node - it has very good HTML tag and css support. You can rotate and scale the WebView node using JavaFX primitives. MathJax can be used within WebView to get high quality equation rendering (if just plain html and css alone doesn't do the job for you). Using JavaFX 2.2, you can take a snapshot of the WebView node and render it to a JavaFX image. You can convert that JavaFX image to an awt BufferedImage using JavaFX 2.2 SwingFXUtils and write it out to a file in many formats using ImageIO.
Here is an example of rendering a piechart node to a png. Depending on the complexity of your html, sometimes an high quality image will compress well to (for example) a png file. In the pichart sample, the 2000x2000 pixel piechart with text and colored gradients saved to a png file of 168kb.
Rewriting the entire application from Swing to JavaFX is not necessary as JavaFX includes the JFXPanel for embedding JavaFX applications inside existing Swing applications. The node snapshot step does not even require the node to be rendered to a screen (it can all be done through memory buffers) - though the JavaFX system would probably need to have been initiated and launched in a JavaFX application or a JFXPanel first.
All of the above may or may not end up giving you the result you want, but it seems a promising avenue to examine.
Update
I ran a couple of tests on this and though I can snapshot a WebView displayed on the screen as explained in this post, due to some limitation of JavaFX 2.2, I was unable to snapshot a WebView displayed as part of an offscreen scene. This means that the information in this answer is accurate, but only applies to the portion of the HTML which can be displayed in the WebView on a screen; e.g. the technique will not currently work for large documents whose pixel size exceeds the screen pixel size. For some sample code, see https://forums.oracle.com/forums/thread.jspa?threadID=2456191.
After a lot of searching and scraping several pieces together I found that the only problem I had with the example in the Update oracle forum link above was that the size of the webview was fixed and that my css used in the html (not in JavaFX) needed.
overflow-x: hidden;
overflow-y: hidden;
to hide the last scrollbar.
So I come up with the following snapshot method (application with animation just as example of your application):
package application;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javafx.animation.Animation;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
public class WebViewSnapshot extends Application {
BorderPane rootPane;
TranslateTransition animation;
#Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(50, 50, 50, 50);
rect.setFill(Color.CORAL);
animation = createAnimation(rect);
Button snapshotButton = new Button("Take snapshot");
Pane pane = new Pane(rect);
pane.setMinSize(600, 150);
rootPane = new BorderPane(pane, null, null, snapshotButton, new Label("This is the main scene"));
snapshotButton.setOnAction(e -> {
// html file being shown in webview
File htmlFile = new File ("generated/template.html");
// the resulting snapshot png file
File aboutFile = new File ("generated/about.png");
generate(htmlFile, aboutFile, 1280, 720);
});
BorderPane.setAlignment(snapshotButton, Pos.CENTER);
BorderPane.setMargin(snapshotButton, new Insets(5));
Scene scene = new Scene(rootPane);
primaryStage.setScene(scene);
primaryStage.show();
}
private TranslateTransition createAnimation(Rectangle rect) {
TranslateTransition animation = new TranslateTransition(Duration.seconds(1), rect);
animation.setByX(400);
animation.setCycleCount(Animation.INDEFINITE);
animation.setAutoReverse(true);
animation.play();
return animation;
}
public void generate(File htmlFile, File outputFile, double width, double height) {
animation.pause();
// rootPane is the root of original scene in an FXML controller you get this when you assign it an id
rootPane.setEffect(new GaussianBlur());
Stage primaryStage = (Stage)rootPane.getScene().getWindow();
// creating separate webview holding same html content as in original scene
WebView webView = new WebView();
// with the size I want the snapshot
webView.setPrefSize(width, height);
AnchorPane snapshotRoot = new AnchorPane(webView);
webView.getEngine().load(htmlFile.toURI().toString());
Stage popupStage = new Stage(StageStyle.TRANSPARENT);
popupStage.initOwner(primaryStage);
popupStage.initModality(Modality.APPLICATION_MODAL);
// this popup doesn't really show anything size = 1x1, it just holds the snapshot-webview
popupStage.setScene(new Scene(snapshotRoot, 1, 1));
// pausing to make sure the webview/picture is completely rendered
PauseTransition pt = new PauseTransition(Duration.seconds(2));
pt.setOnFinished(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
WritableImage image = webView.snapshot(null, null);
// writing a png to outputFile
// writing a JPG like this will result in a pink JPG, see other posts
// if somebody can scrape me simple code to convert it ARGB to RGB or something
String format = "png";
try {
ImageIO.write(SwingFXUtils.fromFXImage(image, null), format, outputFile);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rootPane.setEffect(null);
popupStage.hide();
animation.play();
}
}
});
// pausing, after pause onFinished event will take + write snapshot
pt.play();
// GO!
popupStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Create an image from a non-visible AWT Component?

I'm trying to create an image (screen-shot) of a non-visible AWT component. I can't use the Robot classes' screen capture functionality because the component is not visible on the screen. Trying to use the following code:
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
component.paintAll(g);
Works sometimes, but does not work if the component contains things such as a text box or button, or some sort of OpenGL / 3D component (these things are left out of the image!). How can I take a proper screenshot of the whole thing?
(disclamer: woops.. this doesn't seem to work for AWT )-:
I can't believe no one has suggested SwingUtilities.paintComponent or CellRendererPane.paintComponent which are made for this very purpose. From the documentation of the former:
Paints a component to the specified Graphics. This method is primarily useful to render Components that don't exist as part of the visible containment hierarchy, but are used for rendering.
Here is an example method that paints a non-visible component onto an image:
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class ComponentPainter {
public static BufferedImage paintComponent(Component c) {
// Set it to it's preferred size. (optional)
c.setSize(c.getPreferredSize());
layoutComponent(c);
BufferedImage img = new BufferedImage(c.getWidth(), c.getHeight(),
BufferedImage.TYPE_INT_RGB);
CellRendererPane crp = new CellRendererPane();
crp.add(c);
crp.paintComponent(img.createGraphics(), c, crp, c.getBounds());
return img;
}
// from the example of user489041
public static void layoutComponent(Component c) {
synchronized (c.getTreeLock()) {
c.doLayout();
if (c instanceof Container)
for (Component child : ((Container) c).getComponents())
layoutComponent(child);
}
}
}
Here is a snippet of code that tests the above class:
JPanel p = new JPanel();
p.add(new JButton("Button 1"));
p.add(new JButton("Button 2"));
p.add(new JCheckBox("A checkbox"));
JPanel inner = new JPanel();
inner.setBorder(BorderFactory.createTitledBorder("A border"));
inner.add(new JLabel("Some label"));
p.add(inner);
BufferedImage img = ComponentPainter.paintComponent(p);
ImageIO.write(img, "png", new File("test.png"));
And here is the resulting image:
Component has a method paintAll(Graphics) (as you already have found). That method will paint itself on the passed graphics. But we have to pre-configure the graphics before we call the paint method. That's what I found about the AWT Component rendering at java.sun.com:
When AWT invokes this method, the
Graphics object parameter is
pre-configured with the appropriate
state for drawing on this particular
component:
The Graphics object's color is set to the component's foreground property.
The Graphics object's font is set to the component's font property.
The Graphics object's translation is set such that the coordinate (0,0) represents the upper left corner of the component.
The Graphics object's clip rectangle is set to the area of the component that is in need of repainting.
So, this is our resulting method:
public static BufferedImage componentToImage(Component component, Rectangle region)
{
BufferedImage img = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE);
Graphics g = img.getGraphics();
g.setColor(component.getForeground());
g.setFont(component.getFont());
component.paintAll(g);
g.dispose();
if (region == null)
{
return img;
}
return img.getSubimage(region.x, region.y, region.width, region.height);
}
This is also the better way instead of using Robot for the visible components.
EDIT:
A long time ago I used the code I posted here above, and it worked, but now not. So I searched further. I have a tested, working way. It is dirty, but works. The Idea of it is making a JDialog, put it somewhere out of the Screen bounds, set it visible, and then draw it on the graphics.
Here is the code:
public static BufferedImage componentToImageWithSwing(Component component, Rectangle region) {
BufferedImage img = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics g = img.createGraphics();
// Real render
if (component.getPreferredSize().height == 0 && component.getPreferredSize().width == 0)
{
component.setPreferredSize(component.getSize());
}
JDialog f = new JDialog();
JPanel p = new JPanel();
p.add(component);
f.add(p);
f.pack();
f.setLocation(-f.getWidth() - 10, -f.getHeight() -10);
f.setVisible(true);
p.paintAll(g);
f.dispose();
// ---
g.dispose();
if (region == null) {
return img;
}
return img.getSubimage(region.x, region.y, region.width, region.height);
}
So, this will work also on Windows and Mac. The other answer was to draw it on a virtual screen. But this doesn't need it.
Excellent question, I've thought about this myself from time to time!
As you already have written, that rending heavy weight components such as 3D and AWT onto an image is a big problem. These components are (almost) directly transferred to the graphic card so they cannot be re-rendered to an image using the normal paintComponent stuff, you need help from the operative system or doing your own rendering of these components.
1. Making your own to image renderer
For each component that does not have a to image rendering method you need to create your own. For example using jogl you can take a off-screen screenshot using this method (SO post).
2. Rendering onto a virtual screen
Prerequisites:
Can you start the program/component in a headless environment?
Are you using Linux?
Then you can use Xvfb to render the whole program onto a virtual screen and then taking a screenshot from that virtual screen like this:
Xvfb :1 &
DISPLAY=:1 java YourMainClass
xwd -display :1 -root -out image.xwd
Maybe you need to tweek Xvfb a little bit by passing the size of the program you want to render to it (-screen 0 1024x768x24).
The Screen Image class shows how this can be done for Swing components. I've never tried it with AWT components, buy I could guess the concept would be the same.
How about something like this. The JFrame that holds all of the components is not visible.
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
/**
* Captures an invisible awt component
* #author dvargo
*/
public class ScreenCapture
{
private static List<String> types = Arrays.asList( ImageIO.getWriterFileSuffixes() );
/**
* Build GUI
* #param args
*/
public static void main(String [] args)
{
JFrame invisibleFrame = new JFrame();
invisibleFrame.setSize(300, 300);
JPanel colorPanel = new JPanel();
colorPanel.setBackground(Color.red);
colorPanel.setSize(invisibleFrame.getSize());
JTextArea textBox = new JTextArea("Here is some text");
colorPanel.add(textBox);
invisibleFrame.add(colorPanel);
JButton theButton = new JButton("Click Me");
colorPanel.add(theButton);
theButton.setVisible(true);
textBox.setVisible(true);
colorPanel.setVisible(true);
//take screen shot
try
{
BufferedImage screenShot = createImage((JComponent) colorPanel, new Rectangle(invisibleFrame.getBounds()));
writeImage(screenShot, "filePath");
}
catch (IOException ex)
{
Logger.getLogger(ScreenCapture.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* Create a BufferedImage for Swing components.
* All or part of the component can be captured to an image.
*
* #param component component to create image from
* #param region The region of the component to be captured to an image
* #return image the image for the given region
*/
public static BufferedImage createImage(Component component, Rectangle region) {
// Make sure the component has a size and has been layed out.
// (necessary check for components not added to a realized frame)
if (!component.isDisplayable()) {
Dimension d = component.getSize();
if (d.width == 0 || d.height == 0) {
d = component.getPreferredSize();
component.setSize(d);
}
layoutComponent(component);
}
BufferedImage image = new BufferedImage(region.width, region.height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
// Paint a background for non-opaque components,
// otherwise the background will be black
if (!component.isOpaque()) {
g2d.setColor(component.getBackground());
g2d.fillRect(region.x, region.y, region.width, region.height);
}
g2d.translate(-region.x, -region.y);
component.paint(g2d);
g2d.dispose();
return image;
}
public static void layoutComponent(Component component) {
synchronized (component.getTreeLock()) {
component.doLayout();
if (component instanceof Container) {
for (Component child : ((Container) component).getComponents()) {
layoutComponent(child);
}
}
}
}
/**
* Write a BufferedImage to a File.
*
* #param image image to be written
* #param fileName name of file to be created
* #exception IOException if an error occurs during writing
*/
public static void writeImage(BufferedImage image, String fileName)
throws IOException
{
if (fileName == null) return;
int offset = fileName.lastIndexOf( "." );
if (offset == -1)
{
String message = "file suffix was not specified";
throw new IOException( message );
}
String type = fileName.substring(offset + 1);
if (types.contains(type))
{
ImageIO.write(image, type, new File( fileName ));
}
else
{
String message = "unknown writer file suffix (" + type + ")";
throw new IOException( message );
}
}
}

Categories