Trying to do something relatively simple, given a 512x512 png of a map, I'm attempting to plot points. My code's fairly straightforward, I've tried using both the setRGB function and the Graphics2D object that is returned by the createGraphics function. I must be overlooking something simple. EDIT: I should mention that I'm not looking to create a new BufferedImage, I'm looking to modify the existing BufferedImage, since successive library calls will continue to modify the BufferedImage that I'm working with. (In the example code below, I read the BufferedImage from a file, for a simple way to replicate the issue.
File outputImage = new File("before.png");
BufferedImage img = ImageIO.read(outputImage);
img.setRGB(255, 255, new Color(0f, 1f, 0).getRGB());
File after = new File("after.png");
ImageIO.write(img, "png", after);
If you zoom in on the resulting pixel, it's not green, but some darker grey. Since this behavior is uniform with the Graphics2D, I'm hoping solving this problem will address that as well.
The color space of the BufferedImage must be causing a problem.
In the code below I use your original image and paint it to a BufferedImage with the specified color space:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import java.io.*;
import javax.imageio.*;
import java.net.*;
public class SSCCE extends JPanel
{
SSCCE()
{
try
{
BufferedImage original = ImageIO.read( new File("map.png") );
int width = original.getWidth(null);
int height = original.getHeight(null);
int size = 100;
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = bi.createGraphics();
g2d.drawImage(original, 0, 0, null);
int color = new Color(0f, 1f, 0f).getRGB();
bi.setRGB(10, 10, color);
bi.setRGB(10, 11, color);
bi.setRGB(11, 10, color);
bi.setRGB(11, 11, color);
add( new JLabel( new ImageIcon(bi) ) );
}
catch(Exception e2) {}
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("SSCCE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new SSCCE());
frame.pack();
frame.setLocationByPlatform( true );
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater( () -> createAndShowGUI() );
/*
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
*/
}
}
Here's an attempt to describe a few ways you could work around the problem, based on discussion in the comments:
The problem is that the original image is using an IndexColorModel (or a color map or "palette" if you like). There is no green color that exactly matches the color you specify, so instead the color model does a lookup to get the "closest" color to the one you specified (you may not agree to this color being the closest match, but it is given the algorithm used).
If you set the color to one matching the colors of the image, you can paint in that color. Try new Color(0.8f, 0.9019608f, 0.6392157f) or RGB value 0xffcce6a3 for the light green one. Use new Color(0.6392157f, 0.8f, 1f) or 0xffa3ccff for the light blue.
If you wonder how I found those values, here's the explanation. Assuming colorModel is an IndexColorModel, you can use:
int[] rgbs = new int[colorModel.getMapSize()];
colorModel.getRGBs(rgbs);
...to get the colors in the color map. Choosing one of these colors should always work.
Now, if your "library" (which you haven't disclosed much details about) is using a fixed palette for generating these images, you are good, and can use one of the colors I mentioned, or use the approach described to get the colors, and choose an appropriate one. If not, you need to dynamically find the best color. And if you're really out of luck, there might be no suitable color available at all (ie., your map tile is all ocean, and the only color available is sea blue, it will be impossible to plot a green dot). Then there's really no other way to solve this, than to modify the library.
A completely different approach, could be similar to #camickr's solution, where you temporarily convert the image to true color (BufferedImage.TYPE_INT_RGB or TYPE_3BYTE_BGR), paint your changes onto this temporary image, then paint that image back onto the original. The reason why this might work better, is that the composing mechanism will use a dither and a better color lookup algorithm. But you'll still have the same issue related to available colors as described in the previous paragraph.
Here's a code sample, using the warm yellow color, and the output:
Color color = new Color(0.89411765f, 0.5686275f, 0.019607844f);
int argb = color.getRGB();
Graphics2D g = image.createGraphics();
try {
g.setColor(color);
g.fillRect(10, 10, 50, 50);
}
finally {
g.dispose();
}
Related
I am stuck (beyond the limits of fun) at trying to fix text quality with offscreen image double buffering.
Screen capture worth a thousand words.
The ugly String is drawn to an offscreen image, and then copied to the paintComponent's Graphics argument.
The good looking String is written directly to the paintComponent's Graphics argument, bypassing the offscreen image.
Both Graphics instances (onscreen and offscreen) are identically setup in terms of rendering quality, antialiasing, and so on...
Thank you very much in advance for your wisdom.
The very simple code follows:
public class AcceleratedPanel extends JPanel {
private Dimension osd; //offscreen dimension
private BufferedImage osi; //offscreen image
private Graphics osg; //offscreen graphic
public AcceleratedPanel() {
super();
}
#Override
public final void paintComponent(Graphics g) {
super.paintComponent(g);
// --------------------------------------
//OffScreen painting
Graphics2D osg2D = getOffscreenGraphics();
setupGraphics(osg2D);
osg2D.drawString("Offscreen painting", 10, 20);
//Dump offscreen buffer to screen
g.drawImage(osi, 0, 0, this);
// --------------------------------------
// OnScreen painting
Graphics2D gg = (Graphics2D)g;
setupGraphics(gg);
gg.drawString("Direct painting", 10, 35);
}
/*
To make sure same settings are used in different Graphics instances,
a unique setup procedure is used.
*/
private void setupGraphics(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
private Graphics2D getOffscreenGraphics() {
//Graphics Acceleration
Dimension currentDimension = getSize();
if (osi == null || !currentDimension.equals(osd)) {
osi = (BufferedImage)createImage(currentDimension.width, currentDimension.height);
osg = osi.createGraphics();
osd = currentDimension;
}
return (Graphics2D) osg;
}
} //End of mistery
You are not drawing your two strings with the same color. The default color for the offscreen Graphics is rgb(0, 0, 0) (that is, pure black), while Swing will set the color of a Graphics object to the look-and-feel’s default color—which, for me on Windows 7, using the default theme, is rgb(51, 51, 51), or dark gray.
Try placing g.setColor(Color.BLACK); in your setupGraphics method, to ensure both strings are drawn with the same color.
Thanks for the replies.
With mentioning DPI, MadProgrammer has lead me to a working fix which I offer here more as workaround than as a 'clean' solution to be proud of. It solves the issue, anyway.
I noticed that while my screen resolution is 2880x1800 (Retina Display), MouseEvent's getPoint() method reads x=1440, y=900 at the lower right corner of the screen. Then, the JPanel size is half the screen resolution, although it covers the full screen.
This seen, the solution is as follows:
first, create an offscreen image matching the screen resolution, not the JPanel.getSize() as suggested in dozens of double buffering articles.
then, draw in the offscreen image applying a magnifying transform, bigger than needed, in particular scaling by r = screen dimension / panel dimension ratio.
finally, copy a down scaled version of the offscreen image back into the screen, applying a shrinking factor of r (or scaling factor 1/r).
The solution implementation is split into two methods:
An ammended version of the initial paintComponent posted earlier,
a helper method getDPIFactor() explained afterwards.
The ammended paintComponent method follows:
public final void paintComponent(Graphics g) {
super.paintComponent(g);
double dpiFactor = getDPIFactor();
// --------------------------------------
//OffScreen painting
Graphics2D osg2D = getOffscreenGraphics();
setupGraphics(osg2D);
//Paint stuff bigger than needed
osg2D.setTransform(AffineTransform.getScaleInstance(dpiFactor, dpiFactor));
//Custom painting
performPainting(osg2D);
//Shrink offscreen buffer to screen.
((Graphics2D)g).drawImage(
osi,
AffineTransform.getScaleInstance(1.0/dpiFactor, 1.0/dpiFactor),
this);
// --------------------------------------
// OnScreen painting
Graphics2D gg = (Graphics2D)g;
setupGraphics(gg);
gg.drawString("Direct painting", 10, 35);
}
To complete the task, the screen resolution must be obtained.
A call to Toolkit.getDefaultToolkit().getScreenResolution() doesn't solve the problem, as it returns the size of a JPanel covering the whole screen. As seen above, this figure doesn't match the actual screen size in physical dots.
The way to get this datum is cleared by Sarge Bosch in this stackoverflow post.
I have adapted his code to implement the last part of the puzzle, getDPIFactor().
/*
* Adapted from Sarge Bosch post in StackOverflow.
* https://stackoverflow.com/questions/40399643/how-to-find-real-display-density-dpi-from-java-code
*/
private Double getDPIFactor() {
GraphicsDevice defaultScreenDevice =
GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice();
// on OS X, it would be CGraphicsDevice
if (defaultScreenDevice instanceof CGraphicsDevice) {
CGraphicsDevice device = (CGraphicsDevice) defaultScreenDevice;
// this is the missing correction factor.
// It's equal to 2 on HiDPI a.k.a. Retina displays
int scaleFactor = device.getScaleFactor();
// now we can compute the real DPI of the screen
return scaleFactor * (device.getXResolution() + device.getYResolution()) / 2
/ Toolkit.getDefaultToolkit().getScreenResolution();
} else
return 1.0;
}
This code solves the issue for Mac Retina displays, but I am affraid nowhere else, since CGraphicsDevice is an explicit mention to a proprietary implementation of GraphicsDevice.
I do not have other HDPI hardware with which to play around to have a chance to offer a wider solution.
I need a function/method that can mold(crop and resize) an imported (.png format) image into a circle of exact 150x150 pixels and it should keep transparency. I have searched all over internet, also I have my own code but I think its completely useless. I need this function for a code I am using to make GUI of a social-media app database.
private ImageIcon logo = new ImageIcon(getClass().getResource("/test/test200x200.png"));
toCircle(logo);
I need the code for the following function:
public ImageIcon toCircle(ImageIcon icon)
{
//code
return icon;
}
This function should convert this picture:
To this:
Create a new transparent image
Get a Graphics object from the image.
Set a clip for the graphics object.
Paint the PNG format image.
See also this answer that uses a clipped region.
An alternative approach, that might be more straight-forward to implement for this use case, is:
Create a transparent BufferedImage the size of your icon
Create Graphics2D from image, set hints for antialias
Fill a circle the size of your background circle
Draw the image on top of your circle, using AlphaComposite.SrcIn
Something like:
public Icon toCircle(ImageIcon logo) {
BufferedImage image = new BufferedImage(150, 150); // Assuming logo 150x150
Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.fillOval(1, 1, 148, 148); // Leaving some room for antialiasing if needed
g.setComposite(AlphaComposite.SrcIn);
g.drawImage(logo.getImage(), 0, 0, null);
g.dispose();
return new ImageIcon(image);
}
There is a method called scale(double sx, double sy) in Graphics2D in Java. But this method seems like to scale images as separate surfaces rather than a single surface. As a result, scaled images have sharp corners if original images have no extra width and height. The following screenshot demonstrates the problem:
Here is the code:
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class TestJava {
static int scale = 10;
public static class Test extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.scale(scale, scale);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
BufferedImage img = null;
try {
img = ImageIO.read(new File("Sprite.png"));
} catch (IOException e) {
e.printStackTrace();
}
g2.drawImage(img, null, 5, 5);
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Test test = new Test();
test.setBackground(Color.WHITE);
frame.add(test);
frame.setSize(300, 350);
frame.setVisible(true);
}
}
One possible solution to the problem is original images having extra width and height (in this case "Sprite.png"). But this does not seem to be a good way to eliminate the problem. So I am seeking for a programmatic way in Java to solve this problem rather than using an image editor. What is the way to do so?
In your example it's not the image you scale, but you set a scaling transformation on the Graphics2D object which will be applied on all operations performed on that graphics context.
If you want to scale an image, you have 2 options. All I write below uses java.awt.Image, but since BufferedImage extends Image, all this applies to BufferedImage as well.
1. Image.getScaledInstance()
You can use the Image.getScaledInstance(int width, int height, int hints) method. The 3rd parameter (the hints) tells what scaling algorithm you want to use which will affect the "quality" of the scaled image. Possible values are:
SCALE_DEFAULT, SCALE_FAST, SCALE_SMOOTH, SCALE_REPLICATE, SCALE_AREA_AVERAGING
Try the SCALE_AREA_AVERAGING and the SCALE_SMOOTH for nicer scaled images.
// Scaled 3 times:
Image img2 = img.getScaledInstance(img.getWidth(null)*3, img.getHeight(null)*3,
Image.SCALE_AREA_AVERAGING);
// Tip: you should cache the scaled image and not scale it in the paint() method!
// To draw it at x=100, y=200
g2.drawImage(img2, 100, 200, null);
2. Graphics.drawImage()
You can use different Graphics.drawImage() overloads where you can specify the size of the scaled image. You can "control" the image quality with the KEY_INTERPOLATION rendering hint. It has 3 possible values:
VALUE_INTERPOLATION_NEAREST_NEIGHBOR, VALUE_INTERPOLATION_BILINEAR,
VALUE_INTERPOLATION_BICUBIC
The VALUE_INTERPOLATION_BILINEAR uses a bilinear interpolation algorithm of the 4 nearest pixels. The VALUE_INTERPOLATION_BICUBIC uses a cubic interpolation of the 9 nearby pixels.
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// To draw image scaled 3 times, x=100, y=200:
g2.drawImage(img, 100, 200, img.getWidth(null)*3, img.getHeight(null)*3, null);
Removing sharp edges
If you want to avoid sharp edges around the image, you should write a loop to go over the pixels at the edge of the image, and set some kind of transparency, e.g. alpha=0.5 (or alpha=128). You might also do this on multiple rows/columns, e.g. 0.8 alpha for the edge, 0.5 alpha for the 2nd line and 0.3 alpha for the 3rd line.
An interesting question (+1). I think that it is not trivial to find a good solution for this: The interpolation when scaling up the image always happens inside the image, and I can not imagine a way to make it blur the scaled pixels outside the image.
This leads to fairly simple solution: One could add a 1-pixel-margin around the whole image. In fact, this is the programmatic way of the solution that you proposed yourself. The resuld would look like this:
(the left one is the original, and the right one has the additional 1-pixel-border)
Here as a MCVE, based on your example
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
public class ScaledPaint
{
static int scale = 10;
public static class Test extends JPanel
{
BufferedImage image = createTestImage();
BufferedImage imageWithMargin = addMargin(image);
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.scale(scale, scale);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.drawImage(image, 5, 5, null);
g2.drawImage(imageWithMargin, 30, 5, null);
}
}
private static BufferedImage createTestImage()
{
BufferedImage image =
new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.RED);
g.drawOval(0, 0, 19, 19);
g.dispose();
return image;
}
private static BufferedImage addMargin(BufferedImage image)
{
return addMargin(image, 1, 1, 1, 1);
}
private static BufferedImage addMargin(BufferedImage image,
int left, int right, int top, int bottom)
{
BufferedImage newImage =
new BufferedImage(
image.getWidth() + left + right,
image.getHeight() + top + bottom,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, left, top, null);
g.dispose();
return newImage;
}
private static BufferedImage convertToARGB(BufferedImage image)
{
BufferedImage newImage =
new BufferedImage(image.getWidth(), image.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return newImage;
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
Test test = new Test();
test.setBackground(Color.WHITE);
frame.add(test);
frame.setSize(600, 350);
frame.setVisible(true);
}
}
But...
... one problem with this approach can already be seen in the screenshot: The image becomes larger. And you'll have to take this into account when painting the image. So if your original sprites all had a nice, predefined, easy-to-handle size like 16x32, they will afterwards have a size of 18x34, which is rather odd for a tile. This may not a problem, depending on how you are handling your tile sizes. But if it is a problem, one could think about possible solutions. One solution might be to ...
take the 16x32 input image
create a 16x32 output image
paint the 16x32 intput image into the region (1,1)-(15,31) of the output image
But considering the fact that in sprites of this size, every single pixel may be important, this may have undesirable effects as well...
An aside: Altough I assume that the code that you posted was only intended as a MCVE, I'd like to point out (for others who might read this question and the code) :
You shoud NOT load images in the paintComponent method
For efficient painting, any PNG that is loaded (particularly when it contains transparency) should be converted into an image with a known type. This can be done with the convertToARGB method in my code snippet.
I am making a 2d engine using Java based on entities. The physics and sprites are done, but I still need to be able to draw text with the BaseText class. For experimental purposes I am using the following code in the Renderer class (that handles drawing all the sprites and such):
BufferGraphics.drawString(((BaseText) Entity).getText(), (int) -(Origin.getX() * PositionTransform), (int) -Origin.getY());
I would like to, however, be able to either move this code into the setText(final String Text) method of the BaseText entity, i.e. when it is called a new image is created containing the text specified (possibly in different fonts and sizes and such, I haven't decided).
My problem is this: I would like to be able to resize (scale) the text to my liking. It would also be nice to have the text converted to an image as I can get the dimensions of it and set the size of the text entity itself.
Basically, what I need follows something along these lines:
Take desired string and feed it into the setText method.
Take the string and draw it onto an image, sized so that the text will fit into it exactly.
Set this new image to the Image field in the entity so that the engine can draw it.
Is this even possible? There may be a way to do this with the FontMetrics class or whatever it may be called, but I'm not so sure as I have not used it before.
Edit : Let me clarify: I want to create a BufferedImage based on the size of some text set to a specific font and size, not size the text to fit an image.
Edit 2: Thanks to this fellow Andrew, whom so graciously provided code, I was able to add some code to the engine that, by all means, just plain should work. Again, however, not even with that drawRect in there, the image either remains either transparent or somehow is not getting drawn. Let me supply some breadcrumbs: -snip-
The stupid thing is that all the other sprites and images and such draw fine, so I am not sure how it could be the Renderer.
By the way, that was the paint() method.
Edit 3:
...
Uh...
...
Oh my.
I am...
...
Text can not explain how hard I belted myself in the face with my left palm.
BaseText.java
#Override
public BufferedImage getImage() {return null;}
Renderer.java
BufferedImage Image = Entity.getImage();
I am
a huge idiot.
Thank you, Andrew, for that code. It worked fine.
Edit 4: By the way, here's the final code that I used:
public void setText(final String Text)
{
Graphics2D Draw = (Graphics2D) Game.View.getBuffer().getDrawGraphics();
FontMetrics Metrics = Draw.getFontMetrics();
Rectangle2D Bounds = Metrics.getStringBounds(Text, Draw);
BufferedImage NewImage = new BufferedImage((int) Bounds.getWidth(), (int) (Bounds.getHeight() + Metrics.getDescent()), BufferedImage.TYPE_INT_RGB);
Draw = (Graphics2D) NewImage.getGraphics();
Draw.setColor(new Color(0xAAFF0000));
Draw.drawRect(0, 0, NewImage.getWidth(), NewImage.getHeight());
Draw.drawString(Text, 0, (int) Bounds.getHeight());
this.Image = NewImage;
this.Text = Text;
this.setSize(new Vector(NewImage.getWidth(), NewImage.getHeight()));
}
Use FontMetrics, GlyphView or the preferred size a JLabel (handy for getting the size needed to display formatted text.
Adjust the sizes of the font in step 1 until it fits. Call BufferedImage.createGraphics() to get a Graphics2D object. Paint the String to that.
I do not understand point 3, so won't comment.
Here is how it would work with either FontMetrics or a JLabel.
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.Rectangle2D;
import javax.swing.*;
class TextSize {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Technique 1 - FontMetrics
String s = "The quick brown fox jumps over the lazy dog!";
BufferedImage bi = new BufferedImage(
1,
1,
BufferedImage.TYPE_INT_RGB);
Graphics g = bi.getGraphics();
FontMetrics fm = g.getFontMetrics();
Rectangle2D b = fm.getStringBounds(s,g);
System.out.println(b);
bi = new BufferedImage(
(int)b.getWidth(),
(int)(b.getHeight() + fm.getDescent()),
BufferedImage.TYPE_INT_RGB);
g = bi.getGraphics();
g.drawString(s,0,(int)b.getHeight());
JOptionPane.showMessageDialog(
null,
new JLabel(new ImageIcon(bi)));
// Technique 3 - JLabel
JLabel l = new JLabel(s);
l.setSize(l.getPreferredSize());
bi = new BufferedImage(
l.getWidth(),
l.getHeight(),
BufferedImage.TYPE_INT_RGB);
g = bi.getGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,400,100);
l.paint(g);
JOptionPane.showMessageDialog(
null,
new JLabel(new ImageIcon(bi)));
}
});
}
}
I would like to render a Java Swing component, e.g. a JButton, which I also put on a JFrame, to a BufferedImage. This works in general, but with a major drawback: Text anti aliasing, especially "LCD" anti aliasing mode, is not working when rendering to a BufferedImage.
I've put some example code together to demonstrate the problem, but first my system information:
OS: Windows 7 64 Bit
JVM: 1.6.0_26-b03 (32 Bit)
The following example code will create a simple JFrame, put a JButton on it and then renders the JButton to a file "test.png":
public class TextAntiAliasingTest
{
public TextAntiAliasingTest() throws IOException
{
// Create Test-Button which will be rendered to an image
JButton button = new JButton( "The Test-Button" );
button.setSize( 200, 70 );
button.setLocation( 200, 150 );
// Create JFrame
final JFrame frame = new JFrame();
frame.setSize( 800, 600 );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setLayout( null );
frame.setLocationRelativeTo( null );
frame.add( button );
// Show JFrame
SwingUtilities.invokeLater( new Runnable() {
#Override public void run() {
frame.setVisible( true );
}
});
// Render JButton to an BufferedImage
BufferedImage image = new BufferedImage( 800, 600, BufferedImage.TYPE_INT_ARGB );
Graphics2D g2d = (Graphics2D)image.getGraphics();
button.paint( g2d );
// Write BufferedImage to a PNG file
ImageIO.write( image, "PNG", new File( "test.png" ) );
}
public static void main( String[] args ) throws Exception
{
UIManager.setLookAndFeel( "com.sun.java.swing.plaf.windows.WindowsLookAndFeel" );
System.setProperty( "awt.useSystemAAFontSettings", "lcd" );
new TextAntiAliasingTest();
}
}
The following image shows the difference between the JButton in the JFrame on screen, and the same rendered JButton in the image file:
Actually there is some text anti aliasing in the image, but not the LCD optimized anti aliasing which is shown on screen in the JFrame (this problem occurs also with the default LookAndFeel, not only with the "WindowsLookAndFeel").
I already tried to explicitely set the RenderingHint for text anti aliasing on the "g2d", the Graphics2D context of the BufferedImage like so:
g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB );
But it had no effect at all.
I urgently need to render the JButton to an image file like it is rendered on screen (this is just an example, actually I need to render some more complex components, which all suffer from that anti aliasing problem) and I hope there is a real solution without any nasty workaround like taking a screenshot or something.
I really appreciate any help - thanks a lot!
This is a JVM bug
I have encountered the same problem as you. And I have concluded that the problem is indeed with drawing to a translucent bitmap. I'm fairly certain that we're hitting: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6749069
Workaround
For now I draw into an opaque bitmap and blit it into my destination bitmap. If the background color behind your text is plain, this should work:
private void drawString(String text, int x, int y, Graphics2D g, Color bg){
// Prepare an off-screen image to draw the string to
Rectangle2D bounds = g.getFontMetrics().getStringBounds(text, g);
BufferedImage image = configuration.createCompatibleImage(
(int)(bounds.getWidth() + 1.0f),
(int)(bounds.getHeight() + 1.0f),
Transparency.OPAQUE);
Graphics2D ig = image.createGraphics();
// Fill the background color
ig.setColor(bg);
ig.fillRect(0, 0, image.getWidth(), image.getHeight());
// Draw the string
int x0 = 0;
int y0 = ig.getFontMetrics().getAscent();
ig.setColor(g.getColor());
ig.setRenderingHints(g.getRenderingHints());
ig.setFont(g.getFont());
ig.drawString(text, x0, y0);
ig.dispose();
// Blit the image to the destination
g.drawImage(image, x-x0, y-y0, null);
}
If your background is not a plain color, you'll have to render the text as white on black to a bitmap, A. Then fill another bitmap with your text color C. Then blit that to your destination image, D as: D += A*C or D = D*(1-A)+A*C or some other suitable blending function.
It's probably too late anyway. The problem could be related to a translucent BufferedImage image you're using. The following seems to work Ok:
BufferedImage image = new BufferedImage( 800, 600, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = (Graphics2D)image.getGraphics();
g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
While looking around I have found similar complains.
Either you want (a) your translucent BufferedImage to look as good as your JButton, or (b) you want your JButton and BufferedImage to look the same.
If (b), can you have a translucent JButton? Will it look the same as your translucent BufferedImage?
If (a) then a hard way to do it is, you could maybe paint the JButton to a non-translucent BufferedImage (if Java has a greyscale BufferedImage, use this). This will end up being the negative of your final BufferedImage's alpha channel (a black pixel becomes fully opaque, white fully transparent.) To get the colour for the pixels in the BufferedImage, you'll need to set any pixel that isn't transparent to black - the pixel's transparency should make it come out to be the grey you want.