Drawing a Component to BufferedImage causes display corruption
Summary: The original JScrollNavigator
uses the Swing opacity
property to render a convenient green NavBox
over a scaled thumbnail of the component in an adjacent JScrollPane
. Because it extends JPanel
, the (shared) UI delegate's use of opacity
conflicts with that of the scrollable component. The images seen in edit 5 above typify the associated rendering artifact, also shown here. The solution is to let NavBox
, JScrollNavigator
and the scrollable component extend JComponent
, as suggested in the second addendum below. Each component can then manage it's own properties individually.
I see no unusual rendering artifact with your code as posted on my platform, Mac OS X, Java 1.6. Sorry, I don't see any glaring portability violations.
A few probably irrelevant, but perhaps useful, observations.
Even if you use
setSize()
, appropriately in this case, you should stillpack()
the enclosingWindow
.f.pack();
f.setSize(300, 200);For convenience,
add()
forwards the component to the content pane.f.add(nav, BorderLayout.WEST);
Prefer
StringBuilder
toStringBuffer
.Consider
ComponentAdapter
in place ofComponentListener
.
Addendum: As suggested here, I got somewhat more flexible results using RenderingHints
instead of getScaledInstance()
as shown below. Adding a few icons makes it easier to see the disparate effect on images and text.
editPane.insertIcon(UIManager.getIcon("OptionPane.errorIcon"));
editPane.insertIcon(UIManager.getIcon("OptionPane.warningIcon"));
...
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Component view = jScrollPane.getViewport().getView();
BufferedImage img = new BufferedImage(view.getWidth(),
view.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D off = img.createGraphics();
off.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
off.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
view.paint(off);
Graphics2D on = (Graphics2D)g;
on.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
on.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
on.drawImage(img, 0, 0, getWidth(), getHeight(), null);
}
Addendum secundum: It looks like the JPanel
UI delegate is not cooperating. One workaround is to extend JComponent
so that you can control opacity. It's only slightly more work to manage the backgroundColor
. NavBox
and JScrollNavigator
are also candidates for a similar treatment.
jsp.setViewportView(new JComponent() {
{
setBackground(Color.red);
setBorder(BorderFactory.createLineBorder(Color.BLACK, 16));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
@Override
public Dimension getPreferredSize() {
return new Dimension(300, 300);
}
});
Flickering when drawing onto bufferedImage
You really need to take the time to read through:
- Performing Custom Painting
- Painting in AWT and Swing
- 2D Graphics
- Concurrency in Swing
- How to Use Swing Timers
These aren't "beginner" topics and a reasonable understanding of Swing in general and the language in particular would be very advantageous.
Don't, ever, use getGraphics
on a component. This is simply a bad idea (and I have no idea why this method is public
).
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (IOException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
}
public class TestPane extends JPanel {
private BufferedImage myImg;
private double rotation;
public TestPane() throws IOException {
myImg = ImageIO.read(getClass().getResource("/images/happy.png"));
Timer timer = new Timer(33, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
rotation += Math.toRadians(5);
repaint();
}
});
timer.start();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform at = AffineTransform.getTranslateInstance((getWidth() - myImg.getWidth()) / 2, (getHeight() - myImg.getHeight()) / 2);
at.rotate(rotation, myImg.getWidth() / 2, myImg.getHeight() / 2);
g2d.transform(at);
g2d.drawImage(myImg, 0, 0, this);
g2d.dispose();
}
}
}
Only drawing part of bufferedimage
The probable immediate issue is the fact that HandPanel
does not provide any sizing hints to allow layout managers to determine the best size for the component, fallback on it's default preferred size of 0x0
I went back and modified you code slightly to make it a little more reusable.
img = ImageIO.read(new File("/media/billy/HOME/workspace/Shithead/src/cards/" + toName(c)));
is a bad idea.
You should never reference any path which contains src
, src
won't exist once your application is built. You should not reference embedded resources by File
, they aren't (or at least won't be when the application is build), instead, you need to use Class#getResource
.
public class CardImage {
BufferedImage img = null;
public CardImage(Card card) {
setImage(card);
}
public BufferedImage getImage() {
return img;
}
public BufferedImage setImage(Card c) {
try {
//img = ImageIO.read(new File("/media/billy/HOME/workspace/Shithead/src/cards/" + toName(c)));
img = ImageIO.read(getClass().getResource("/cards/" + toName(c)));
} catch (Exception e) {
System.out.println(e);
}
/*
int scale_factor = 8;
System.out.println(img.getHeight());
Image dimg = img.getScaledInstance((int)img.getWidth()/scale_factor, (int)img.getHeight()/scale_factor, Image.SCALE_SMOOTH);
Graphics g = img.createGraphics();
g.drawImage(dimg, 0, 0, null);
g.dispose();
*/
return img;
}
public String toName(Card c) {
String tmp = c.toString().replaceAll(" ", "_");
tmp = tmp.toLowerCase();
tmp = tmp + ".png";
return tmp;
}
}
I've modified the CardImage
class so it needs to be setup in advance, this allows you to cache the image and re-use it more efficiently. I'd also consider using a factory of some kind which, when passed a Card
could determine if it needs to generate a new instance of CardImage
or re-use a previously cached version, this would make it faster for your code to execute.
Again, I've modified the HandPanel
, it now requires you to pass a Card
to it and it uses this to make determinations about what information it will use to generate sizing hints
public class HandPanel extends JPanel {
CardImage cardImage;
public HandPanel(Card card) {
cardImage = new CardImage(card);
}
@Override
public Dimension getPreferredSize() {
BufferedImage img = cardImage.getImage();
return img == null ? super.getPreferredSize() : new Dimension(img.getWidth(), img.getHeight());
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
BufferedImage img = cardImage.getImage();
g.drawImage(img, 0, 0, this);
}
}
I'd also encourage you to take a look at Perials of getScaledInstance and Java: maintaining aspect ratio of JPanel background image for some alternatives
Graphics.drawImage() consumes a lot of memory drawing an int[] image
There are several issues with the code. Some refer to performance, others to style or best practices, and others (at least potentially) refer to memory consumption.
- Performance: The
getScaledInstance
method is distressingly slow. See https://stackoverflow.com/a/32278737/3182664 and others for better alternatives - Style: It's
imageWidth
, notimage_width
- Best practices: For a
JComponent
, you usually, you overridepaintComponent
and notpaint
- Memory consumption: That's the main point...:
As MadProgrammer already pointed out: Do things as rarely as possible. The role and purpose of this updateCounter
is not entirely clear. I think that the responsibility for updating the image less frequently should be in the class that uses your component - particularly, in the class that calls updateImage
(which should simply be done less often). Maintaining this in the paint
method is not very reliable.
In your current code, it seems like the currentDisplayedImage
is (despite its name) neither displayed nor used in any other way. It may, however, be a good idea to keep it: It will be needed to be filled with the int[]
data, and as a source for the scaled image that might have to be created.
One possible implementation of your class might look as follows:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
public class ImageViewComponent extends JComponent {
private int updateInterval, updateCounter;
private BufferedImage fullImage;
private BufferedImage displayedImage;
/**
* @param width The width of this component
* @param height The height of this component
* @param ui The higher, the less frequent the image will be updated
*/
public ImageViewComponent(int width, int height, int ui) {
setPreferredSize(new Dimension(width, height));
this.updateInterval = ui;
this.updateCounter = 0;
this.fullImage = null;
this.displayedImage =
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
public void setImage(int[] image, int width, int height) {
// Note: The updateInvervall/updateCounter stuff COULD
// probably also be done here...
if (fullImage == null ||
fullImage.getWidth() != width ||
fullImage.getHeight() != height)
{
fullImage = new BufferedImage(
width, height, BufferedImage.TYPE_INT_RGB);
}
fullImage.setRGB(0, 0, width, height, image, 0, width);
scaleImage(fullImage, displayedImage);
repaint();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(displayedImage, 0, 0, this);
}
private static BufferedImage scaleImage(
BufferedImage input, BufferedImage output)
{
double scaleX = (double) output.getWidth() / input.getWidth();
double scaleY = (double) output.getHeight() / input.getHeight();
AffineTransform affineTransform =
AffineTransform.getScaleInstance(scaleX, scaleY);
AffineTransformOp affineTransformOp =
new AffineTransformOp(affineTransform, null);
return affineTransformOp.filter(input, output);
}
}
but note that this does not do this "updateInterval" handling, for the reason mentioned above.
And a side note: Maybe you don't even have to scale the image. If your intention is to have the image always being displayed at the size of the component, then you can simply do
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Draw the FULL image, which, regardless of its size (!)
// is here painted to just fill this component:
g.drawImage(fullImage, 0, 0, getWidth(), getHeight(), null);
}
Usually, drawing a scaled image like this is pretty fast. But depending on many factors, separating the step of scaling and painting the image, like you did, may also be a reasonable option.
How to draw part of a large BufferedImage?
I have a 10000x10000 BufferedImage and I'm looking to draw only part
of it to a Canvas, is there a way to do this using args such as:
Don't use canvas for custom painting in java. use
JComponent
orJPanel
instead. It has a nice functionpaintComponent(Graphics g)
, override it and paint your image inside withg.drawImage(x, y, width, height, observer)
;Swing graphics has
Graphics.clipRect(int x, int y, int width, int height)
to bound the area rectangle to which you wish to draw prior to drawing the image.
Edit (In response to your edited question):
First approach is to use BufferedImage..getSubimage(x, y, width, height)
to get a sub image with specified rectangle region. It is faster.
BufferedImage img = ImageIO.read(new File("file"));
img = img.getSubimage(50, 50, 500, 500); // 500 x 500
This function will give you a new image cropped with the rectangle(x, y, width, height)
of your original image you specified. Use the returned image to draw on your component.
Tutorial resource: Clipping the Drawing Region
Demo: Demonstrating clipping Image with Animation:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.logging.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.Timer;
class MyCanvas extends JPanel implements ActionListener
{
public BufferedImage buffImg;
public Rectangle rectangle;
Random random;
long lastTimeChanged;
int dirX = 1, dirY = 1;
volatile static boolean imageLoading = true;
public MyCanvas() {
random = new Random();
rectangle = new Rectangle(50, 50, 250, 250);
lastTimeChanged = System.currentTimeMillis();
setBackground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if(imageLoading)
{
showWaitForLoading(g);
return;
}
g.clipRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
g.drawImage(buffImg, 0, 0, getWidth(), getHeight(), this);
}
public void showWaitForLoading(Graphics g)
{
Graphics2D g2d = (Graphics2D)g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.DARK_GRAY);
g2d.fillRoundRect(getWidth()/2-100, getHeight()/2-15, 200, 30, 30, 30);
g2d.setColor(Color.WHITE);
g2d.drawString("Loading image...", getWidth()/2 - 45, getHeight()/2 + 3 );
g2d.dispose();
}
@Override
public void actionPerformed(ActionEvent e) {
long endTime = System.currentTimeMillis();
if(endTime - lastTimeChanged > 500)
{
dirX = random.nextInt(2) == 0 ? -1 : 1;
dirY = random.nextInt(2) == 0 ? -1 : 1;
lastTimeChanged = endTime;
}
if(rectangle.x < 0)dirX = 1;
else if(rectangle.x + rectangle.width > getWidth())dirX = -1;
if(rectangle.y < 0)dirY = 1;
else if(rectangle.y + rectangle.height > getHeight())dirY = -1;
rectangle.x = rectangle.x + dirX * 10;
rectangle.y = rectangle.y + dirY * 10;;
repaint();
}
}
public class CustomPainting {
public static void main(String[] args) throws IOException {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
final MyCanvas canvas = new MyCanvas();
JFrame frame = new JFrame();
frame.setSize(new Dimension(500, 500));
frame.add(canvas);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(200, canvas);
timer.start();
new Thread()
{
public void run()
{
try {
canvas.buffImg = ImageIO.read(new URL("http://images6.fanpop.com/image/photos/33400000/Cute-Panda-beautiful-pictures-33434826-500-500.jpg"));
MyCanvas.imageLoading = false;
} catch (IOException ex) {
Logger.getLogger(CustomPainting.class.getName()).log(Level.SEVERE, null, ex);
}
}
}.start();
}
});
}
}
Java BufferedImage returns black image from Canvas
Here is what is wrong:
Graphics2D g2=(Graphics2D)image.getGraphics();
boolean x = false;
while(!x){
x = g2.drawImage(image, 0, 0, null);
}
You take the Graphics
of image
and you draw image
onto that Graphics
. So basically, you are drawing the image
on itself.
what you want is probably more like this:
Graphics2D g2=(Graphics2D)image.getGraphics();
canvas.print(g2);
...
Now, consider the following remarks as well:
- Don't use
Canvas
(AWT) but use insteadJPanel
(and overridepaintComponent
) orJLabel
with aBufferedImage
(draw on theGraphics
of theBufferedImage
and callrepaint()
on theJLabel
) (Swing) - Don't use
getGraphics
on any component, use theGraphics
provided in thepaintComponent
method
Small demo example of what I am talking about:
import java.awt.Desktop;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
public class Test {
JFrame f;
JLabel c;
BufferedImage image;
int x = -1, y = -1;
public Test() {
f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
image = new BufferedImage(1200, 800, BufferedImage.TYPE_INT_ARGB);
c = new JLabel(new ImageIcon(image));
f.add(c);
c.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
// empty
}
@Override
public void mouseDragged(MouseEvent e) {
if (x == -1) {
x = e.getX();
y = e.getY();
}
image.getGraphics().fillOval(x, y, 5, 5);
c.repaint();
x = e.getX();
y = e.getY();
}
});
f.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent evt) {
onExit();
}
public void onExit() {
try {
File output = new File("C:\\test\\canvas.png");
if (!output.getParentFile().exists()) {
output.getParentFile().mkdirs();
}
ImageIO.write(image, "png", output);
Desktop.getDesktop().open(output);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Test paintBrush = new Test();
}
});
}
}
Transparency in bufferedimage objects
@Chuk Lee is right: Unless you change it, the default Graphics2D
composite is AlphaComposite.SrcOver
. This handy tool displays the composite result for a selected rule and a specified pair of color and alpha.
Addendum: One approach is to override paintComponent()
and render both map and circles, but you might be able to make the corners transparent by clearing the alpha:
...
gc.setRenderingHints(rh);
gc.setComposite(AlphaComposite.Clear);
gc.fillRect(0, 0, diameter, diameter);
gc.setComposite(AlphaComposite.Src);
gc.setColor(outsideColor);
...
Does createImage(30, 30)
relate to diameter
? For what component do you override paint()
and invoke super.paintComponent(g)
?
Drawing a rounded rectangle with opacity on a BufferedImage
Here, you see that when the bubble on the left is fading, its rounded corners change shape compared to before fading, whereas the bubble on the right's rounded corners do not change. Indeed, the left bubble is drawn on a BufferedImage which is then drawn on the panel, whereas the right bubble is directly drawn on the panel.
Rather than redrawing the image each time with a different alpha value, create it once and use AlphaComposite to manage the transparency.
Below is an adaptation of your example with three 'bubbles': far left is drawing the image every time changing the foreground color, the two on the right use AlphaComposite
(middle using an image that was created once, far right uses the JPanel
Graphics directly).
public class Test {
public static void main(String[] args) {
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setSize(600, 200);
final BufferedImage image = new BufferedImage(600, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
paintExampleBubble(graphics, 250, 50, foreground);
graphics.dispose();
final JPanel panel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D)g;
final BufferedImage i2 = new BufferedImage(600, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = i2.createGraphics();
paintExampleBubble(graphics, 50, 50, alphaForeground);
graphics.dispose();
g.drawImage(i2, 0, 0, this);
//use Alpha Composite for transparency
Composite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER , alpha );
g2d.setComposite(comp);
g2d.drawImage(image, 0, 0, this);
paintExampleBubble(g2d, 450, 50, foreground);
}
};
javax.swing.Timer timer = new javax.swing.Timer(100, new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
alpha -= 0.05;
if ( alpha < 0 ){
alpha = 1.0f;
}
alphaForeground = new Color(0f, 0f, 0f, alpha);
panel.repaint();
}
});
timer.start();
frame.getContentPane().add(panel);
frame.setVisible(true);
}
private static float alpha = 1.0f;
private static final Color background = new Color(1f, 1f, 1f, 1f);
private static final Color foreground = new Color(0f, 0f, 0f, 1f);
private static Color alphaForeground = new Color(0f, 0f, 0f, alpha);
private static final int borderRadius = 16;
private static final int width = 100;
private static final int height = 50;
private static void paintExampleBubble(Graphics g, int x, int y, Color color) {
g.setColor(background);
g.fillRoundRect(x, y, width, height, borderRadius, borderRadius);
g.setColor(color);
g.drawRoundRect(x, y, width, height, borderRadius, borderRadius);
}
}
On my system I see distortion on far left (managing transparency with foretground color) but not with the AlphaComposite transparency
Related Topics
Explanation of Generic <T Extends Comparable<? Super T>> in Collection.Sort/ Comparable Code
How to Run MySQL In-Memory for Junit Test Cases
Spring - How to Use Multiple Transaction Managers in the Same Application
Stream Filter of 1 List Based on Another List
Extract Integer Part in String
Count Int Occurrences with Java8
Why Does Java Code with an Inner Class Generates a Third Someclass$1.Class File
How to Determine the Primitive Type of a Primitive Variable
How to Modify the Raw Xml Message of an Outbound Cxf Request
Why Is "Out of Range" Not Thrown for 'Substring(Startindex, Endindex)'
Java Unreachable Catch Block Compiler Error
How to Search in a List of Java Object