3

I would like to paint a JPanel into a BufferedImage in headless mode (no GUI on the screen at all).

final JPanel panel = createPanel();
panel.setSize(panel.getPreferredSize());
panel.validate();

//  JFrame frame = new JFrame();
//  frame.getContentPane().add(panel);
//  frame.pack();
//  frame.setVisible(true);

final BufferedImage image = new BufferedImage(
            panel.getBounds().width, 
            panel.getBounds().height, 
            BufferedImage.TYPE_INT_ARGB
);

final Graphics2D gc = image.createGraphics();
gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
gc.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

try {
    panel.paint(gc);
    ...save the image somewhere...
} finally {
    gc.dispose();
}

But I always get empty image until I put the panel into a heavy-weight component and show it on the screen (see the commented code). I don't want to show it, this application runs on server.

Here is SSCCE:

    public class Example {

    private static JPanel createPanel() {
        final JPanel panel = new JPanel(new GridBagLayout());           
        final JLabel label = new JLabel("Yeah, it's working!", SwingConstants.CENTER);
        label.setFont(new Font("Arial", Font.PLAIN, 12));           
        final GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.BOTH;
        constraints.weightx = 1;
        constraints.weightx = 1;
        panel.add(label, constraints);          
        return panel;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                final JPanel panel = createPanel();
                panel.setSize(panel.getPreferredSize());
                panel.validate();

    //              JFrame frame = new JFrame();
    //              frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    //              frame.getContentPane().add(panel);
    //              frame.pack();
    //              frame.setVisible(true);

                final BufferedImage image = new BufferedImage(
                        panel.getBounds().width, 
                        panel.getBounds().height, 
                        BufferedImage.TYPE_INT_ARGB
                );    
                final Graphics2D gc = image.createGraphics();
                gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                gc.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);    
                try {
                    panel.paint(gc);
                    ImageIO.write(image, "png", new File("image.png"));
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    gc.dispose();
                }                   
            }
        });
    }    
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
Jan Krakora
  • 2,500
  • 3
  • 25
  • 52
  • The traditional way to do this under Linux is to run a "fake" X-window provider that doesn't actually display anything. But it does keep Java happy. – Lee Meador Jun 10 '13 at 14:59
  • 1
    For better help sooner, post an [SSCCE](http://sscce.org) – Guillaume Polet Jun 10 '13 at 15:02
  • See [Why does the JTable header not appear in the image?](http://stackoverflow.com/q/7369814/418556) for tips on painting unrealized components. If you cannot get it to work based on that, follow the advice of @GuillaumePolet & post an SSCCE. – Andrew Thompson Jun 10 '13 at 15:07
  • The problem is that your label has a zero-size. See my edited answer with a simple fix (call `doLayout` instead of `validate()`). See also camickr answer – Guillaume Polet Jun 10 '13 at 15:34

4 Answers4

4

Components have a zero size until the component has been realized so the painting methods don't work.

Check out Screen Image. It will handle this problem for you by invoking doLayout() on the panel to make sure all components have a valid size.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • My mistake, it is a zero-size issue but on its sub-components. Your answer is absolutely correct: +1 – Guillaume Polet Jun 10 '13 at 15:35
  • 1
    Yes, that works. I'm wondering why I have to call recursively doLayout() on my own. This should be done by simply validating the component. IMHO, another Swing's pitfall. – Jan Krakora Jun 11 '13 at 08:34
3

Here is a snippet that paints a simple label to an image file and then the image file is opened (if on a Desktop computer).

import java.awt.Desktop;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JLabel;

public class Test {

    public static void main(String[] args) throws IOException {
        JLabel label = new JLabel("Hello world");
        label.setSize(label.getPreferredSize());
        BufferedImage image = new BufferedImage(label.getWidth(), label.getHeight(), BufferedImage.TYPE_INT_ARGB);
        label.paint(image.getGraphics());
        File output = new File("C:\\test\\hello world.png");
        if (!output.getParentFile().exists()) {
            output.getParentFile().mkdirs();
        }
        ImageIO.write(image, "png", output);
        Desktop.getDesktop().open(output);
    }

}

EDIT (with your SSCCE):

Don't call validate() but rather doLayout() on your panel (if you have nested panels, make sure to call it recursively):

import java.awt.Desktop;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;

public class Example {

    private static JLabel label;

    private static JPanel createPanel() {
        final JPanel panel = new JPanel(new GridBagLayout());

        label = new JLabel("Yeah, it's working!", SwingConstants.CENTER);
        label.setFont(new Font("Arial", Font.PLAIN, 12));

        final GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.BOTH;
        constraints.weightx = 1;
        constraints.weightx = 1;
        panel.add(label, constraints);

        return panel;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                final JPanel panel = createPanel();
                panel.setSize(panel.getPreferredSize());
                panel.doLayout();
                System.err.println(label.getSize() + "");
                // JFrame frame = new JFrame();
                // frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                // frame.getContentPane().add(panel);
                // frame.pack();
                // frame.setVisible(true);

                final BufferedImage image = new BufferedImage(panel.getBounds().width, panel.getBounds().height,
                        BufferedImage.TYPE_INT_ARGB);

                final Graphics2D gc = image.createGraphics();
                gc.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                gc.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

                try {
                    panel.paint(gc);
                    File output = new File("image.png");
                    ImageIO.write(image, "png", output);
                    Desktop.getDesktop().open(output);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    gc.dispose();
                }

            }
        });
    }
}
Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117
  • 1
    Again, doLayout() only works if you don't have nested panels. `ScreenImage` does doLayout() recursively to make sure all components on all panels have a size. – camickr Jun 10 '13 at 15:42
  • @camickr Yes indeed, this needs to be done recursively. I did follow your link and read the code of `ScreenImage`. – Guillaume Polet Jun 10 '13 at 15:44
1

Calling addNotify on the root component you are attempting to print/paint will also solve the problem. The crux of the issue seems to be that validation calls short circuit unless the container has a 'peer'. Calling addNotify initializes the peer which allows subsequent calls to Component.validate to function as they normally would in a non-headless scenario.

Submitting this as an alternate solution because calling doLayout won't work in cases where components are more deeply nested due to doLayout not laying out subcomponents. (Although the ScreenImage class mentioned in camickr's answer addresses this by invoking doLayout recursively.)

BonusLord
  • 363
  • 1
  • 7
0

Try to instantiate the BufferedImage with one of it's constructor instead. GraphicsEnvironment class probably has to be used with a real GraphicsEnvironment (screen, ...)

new  BufferedImage(panel.getBounds().width, panel.getBounds().height, BufferedImage.TYPE_INT_ARGB )
gma
  • 2,563
  • 15
  • 14
  • `GraphicsEnvironment.getLocalGraphicsEnvironment()` throws an exception in headless mode – Guillaume Polet Jun 10 '13 at 15:03
  • @GuillaumePolet Yes, but I guess that @Behnil is testing on a computer with a `LocalGraphicsEnvironment` but that may generating incorrect image if there's not GUI (i.e. a `JFrame`). – gma Jun 10 '13 at 15:05
  • It means that even though he has a "real" GE, his code is not working. Eventually, this will not work at all on a headless environment. So the problem does not come from there directly (although I agree that eventually it won't work). – Guillaume Polet Jun 10 '13 at 15:16