Drawing a Component to Bufferedimage Causes Display Corruption

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.

Sample Image

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.

image one

A few probably irrelevant, but perhaps useful, observations.

  • Even if you use setSize(), appropriately in this case, you should still pack() the enclosing Window.

    f.pack();
    f.setSize(300, 200);
  • For convenience, add() forwards the component to the content pane.

    f.add(nav, BorderLayout.WEST);
  • Prefer StringBuilder to StringBuffer.

  • Consider ComponentAdapter in place of ComponentListener.

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.

image two

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.

Sample Image

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).

Sample Image

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, not image_width
  • Best practices: For a JComponent, you usually, you override paintComponent and not paint
  • 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:

  1. Don't use canvas for custom painting in java. use JComponent or JPanel instead. It has a nice function paintComponent(Graphics g), override it and paint your image inside with g.drawImage(x, y, width, height, observer);

  2. 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:

Sample Image

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 instead JPanel (and override paintComponent) or JLabel with a BufferedImage (draw on the Graphics of the BufferedImage and call repaint() on the JLabel) (Swing)
  • Don't use getGraphics on any component, use the Graphics provided in the paintComponent 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



Leave a reply



Submit